Basics of Rule Development


Getting started with rule development is relatively straightforward. In Listing 8-1, I combine the three files of an example EmptyFxCopRule project so you could follow along with me. The simple base class, BaseEmptyFxCopRule, exists solely to derive from the required abstract BaseIntrospectionRule class from the Microsoft.FxCop.Sdk.Introspection namespace in FxCopSdk.dll. BaseIntrospectionRule assumes that the description text and related data reside in an embedded resource. Consequently, the three parameters required by the BaseIntrospectionRule constructor are the type name of the rule, the embedded resource file name that contains the rule descriptions, and the assembly that contains the embedded resource file. Because you'll most likely have more than one rule per assembly, having a simple base class, such as BaseEmptyFxCopRule, means that you can hide the common resource information manipulation in the base class and require derived classes to specify the type name in calling the base class constructor.

The second file, EmptyAssemblyRule.cs, shows a minimal rule that looks at assemblies. I'll talk more about the Check method later. The TargetVisibiliy property is where you specify the type of items you want to see. For example, if you wanted your rule limited to only public items, you'd return TargetVisibilities. ExternallyVisible. The main point to notice in EmptyAssemblyRule.cs is that the name of the class is EmptyAssemblyRule. You can see that in its constructor, EmptyAssemblyRule passed that string into the base class.

The final file in Listing 8-1 is the embedded resource XML file. I did a little format tweaking so you could see the individual elements more easily. The key text is the Rule element TypeName attribute at the top, because that's how the engine matches up the data about the rule with the code for the rule. Whereas there's only one rule shown in the XML file, you will have one Rule element for each rule you develop in an assembly. If you've used Code Analysis, you're probably wondering why all of the elements are required, but the only ones you see are the Rule element, CheckId attribute, and Name element. Whereas the stand-alone FxCop shows you all the elements when you double click a rule, Microsoft simply didn't provide that user interface in the integrated Code Analysis.

The name of the embedded resource XML file must end in "Rules.xml" for the integrated Code Analysis to load and show your rules. The FxCop stand-alone version can handle any named file. If Code Analysis cannot load the rules out of your assembly, in the Code Analysis view, the tree control will not show any child items under your rule assembly name.

Listing 8-1. Minimal Code Analysis Rule Source Files

[View full width]

************** BaseEmptyFxCopRule.CS ************** using System ; using Microsoft.Cci; using Microsoft.FxCop.Sdk; using Microsoft.FxCop.Sdk.Introspection; namespace EmptyFxCopRule { [CLSCompliant(false)] abstract public class BaseEmptyFxCopRule : BaseIntrospectionRule { protected BaseEmptyFxCopRule ( String name ) : base ( name , "EmptyFxCopRule.RuleData" , typeof(BaseEmptyFxCopRule) .Assembly ) { } } } ************** EmptyAssemblyRule.CS ************** namespace EmptyFxCopRule { /// <summary> /// Summary description for EmptyRule. /// </summary> [CLSCompliant ( false )] public class EmptyAssemblyRule : BaseEmptyFxCopRule { public EmptyAssemblyRule ( ) : base ( "EmptyAssemblyRule" ) { } public override TargetVisibilities TargetVisibility { get { return ( TargetVisibilities.All ); } } public override ProblemCollection Check ( Module module ) { return ( null ); } } } ************** EmptyRules.XML ************** <?xml version="1.0" encoding="utf-8" ?> <Rules FriendlyName="Example Empty Rule"> <Rule TypeName="EmptyAssemblyRule" Category="EmptyRule.Simple" Check> <Name> EmptyAssemblyRule: The prose name of the rule goes here. </Name> <Description> EmptyAssemblyRule: Add your description here. </Description> <Owner> EmptyAssemblyRule: This is optional, but is generally the developer. </Owner> <Url> EmptyAssemblyRule: The URL for help containing the rule. </Url> <Resolution> EmptyAssemblyRule: The string that points out the error to the user. </Resolution> <Email> EmptyAssemblyRule: The optional e-mail address of the rule author. </Email> <MessageLevel Certainty="99"> Warning </MessageLevel> <FixCategories> NonBreaking </FixCategories> </Rule> </Rules>



Before you even consider writing the code for your rule, I need to point out a few development tricks. First, note that adding an XML file to the project does not add it as an embedded resource. Consequently, after adding the XML resource file that describes the rule, make sure you set the Build Action to Embedded Resource instead of Content. When creating new rules, even if they are additional rules in an existing assembly, always test to make sure it's set up correctly by loading it into your target environment to see if you have the rule names and descriptions from the XML file displayed. Both Code Analysis and FxCop are not too forgiving if there are issues with your rules XML file. If the environment shows you the appropriate data when you load your rule, you're in good shape. One other hint is to get in the habit of ensuring that the CheckId attribute is always unique across all assemblies. Code Analysis uses that value to save the rule state to the project file. If you have duplicate errors, Code Analysis looks only for the first rule with that number and ignores the others.

The last hint with the XML file concerns the MessageLevel and FixCategories elements. Those must both conform to the MessageLevel and FixCategories enumerations, respectively. Just so you have them, here are the definitions from Reflector:

public enum MessageLevel {       CriticalError = 1,       CriticalWarning = 4,       Error = 2,       Information = 0x10,       None = 0,       Warning = 8 } public enum FixCategories {       Breaking = 1,       DependsOnFix = 2,       NonBreaking = 4,       None = 0 }


The MessageLevel element has a required attribute, Certainty, which seems to be the confidence, measured as a percentage, you have in diagnosing the error. Nearly all the Microsoft rules are 95 percent or higher, but only FxCop shows you the value. If you're going to the trouble of writing a rule and you have a confidence level of only 5 percent in the rule being correct, you may want to reconsider what you're doing.

The All-Important Check Method

As you've probably guessed, the Check method is where all the action is. The BaseIntrospectionRule class defines six different virtual Check methods you can override in your rules, though you'll use the following four almost exclusively:

ProblemCollection Check ( Member member ); ProblemCollection Check ( Module module ); ProblemCollection Check ( Parameter parameter ); ProblemCollection Check ( Resource resource );


As you scan down the list of Check methods, you can see, for example, that if you want a rule that works on embedded resources, you simply override the version Check that takes the Resource type as a parameter. It's a little harder to see, but if you want to check classes and methods, you'd use the version that takes a Member as the parameter. Inside that Check method, you'd use the C# as or Visual Basic TryCast statement to convert the Member type into the specific type you're interested in working with. When I started to write Code Analysis rules, the first ones I wanted to add involved assemblies, and fortunately, those are the easiest to get started with. Consequently, I used the Check method that takes a Module as the parameter.

As I discussed back in Chapter 2, I find it quite odd that the Design Guidelines do not make it mandatory to include an XML documentation file with your assemblies. This is especially annoying when all .NET languages now support creating the file directly from the source code comments. Therefore, I wanted a rule that would check that an assembly always has documentation associated with it.

This very important rule, AssembliesHaveXmlDocCommentFiles, turns out to be extremely simple to write. You can find the code in the eponymous .cs file in the .\Wintellect.FxCop.DesignRules directory. To show you how simple the rule is, Listing 8-2 shows the Check method that does all the work. As you can see, if you return null/Nothing in the Check method, that tells Code Analysis that you didn't find any errors. Returning an allocated Problems array signals that there are errors and tells you the text of the error. In Listing 8-2, I use the BaseIntrospectionRule.GetResolution method to get the Resolution element out of the AssembliesHaveXmlDocCommentFiles rule's XML file. In this case, the string is: "'{0}' is missing or has an invalid XML Doc Comments file." The format item allows me to put the name of the assembly in the error text. The BaseIntrospectionRule.GetResolution method takes variable-length arguments, so you can get as much detail as you need into the error message. What's nice about the Problems property in the BaseIntrospectionRule is that if you have a rule that is recursive, you can add all the problems you've found in the code to it and take care of getting them all at the end. With most rules returning only a single error string, you'll usually use code like that specified in the error portion of Listing 8-2 to do the work.

Listing 8-2. Check Method for AssembliesHaveXmlDocCommentFiles Rule

[View full width]

public override ProblemCollection Check ( Module module ) { if ( null == module ) { throw new ArgumentNullException ( Constants.InvalidModule ); } // Is this module an assembly? if ( NodeType.Assembly != module.NodeType ) { return ( null ); } // First, check if this is a DLL library. if ( false == IsLibraryAssembly ( module .Location ) ) { // It's not a DLL, so there's no need for an XML file. return ( null ); } // Build up the name to the XML file, which should be in the same // directory as the DLL. String XmlFile = Path.ChangeExtension ( module.Location , XMLExtension ); bool failedTests = false; if ( true == File.Exists ( XmlFile ) ) { // The file exists, so lets see if it's a proper and happy XML // file. try { XmlDocCommentsFileDictionary tempFile = new XmlDocCommentsFileDictionary ( XmlFile ); tempFile = null; // I guess you could optionally check that the timestamps // are close here, but there are no guarantees that the .XML // file has been edited or even produced by a different // tool such as Innovasys Document! X. } catch ( XmlException ) { failedTests = true; } } else { failedTests = true; } if ( true == failedTests ) { // Build up the error string to return. String fileName = Path.GetFileName ( module.Location ); Resolution res = GetResolution ( fileName ); Problem prob = new Problem ( res ); Problems.Add ( prob ); } return ( Problems ); }



Although AssembliesHaveXmlDocCommentFiles is relatively easy, I wanted to create other rules that would look to see if an assembly had the AssemblyTitleAttribute, AssemblyCompanyAttribute, AssemblyDescriptionAttribute, and AssemblyCopyrightAttribute all applied and not set to empty strings. At first, I thought it was going to be pretty easy to do this because I had just to decompile the Microsoft rule MarkAssembliesWithAssemblyVersion in DesignRules.dll. A quick peek at the rule showed that the Module passed to the Check method had a Version property on it. My hope was that there were either properties to get the attributes I wanted or a simple way to get attributes for an assembly. Of course, life wasn't so easy, but it turned out to be a good way to learn about the Introspection engine usage.

In a previous Reflector exploration mission, I saw that the Microsoft.FxCop.Sdk.Introspection.RuleUtilities class had several GetCustomAttributes methods that pointed out an Attributes property on the various Microsoft.Cci namespace types. With Reflector's wonderful Analyzer, I just had to click on the Used By node to see who called the method I was interested in using. Looking through the various rules files supplied with Code Analysis, you'll see that all the calls to any RuleUtilities.GetCustomAttributes method were using a set of predetermined attribute types defined in the Microsoft.Cci.SystemTypes class as static public fields of type Microsoft.Cci.Class. The Microsoft.Cci.SystemTypes class is the class that represents types in the System.* namespaces. Poking through the Microsoft.Cci.SystemTypes class, I saw that the FxCop team had already done the work I needed because they already had fields that returned the TypeNode class for the attribute classes I wanted. That saved a huge amount of work on my part.

The second parameter expected by GetCustomAttributes is of type Microsoft.Cci.TypeNode, which is perfect because it is the base class for all type items, such as Microsoft.Cci.Class (yes, the same as the fields of SystemTypes representing the attributes I'm interested in), Microsoft.Cci.EnumNode, and Microsoft.Cci.Reference. Therefore, my next mission was to find an example of getting a type out of a Module class to see what method to call.

To tie everything together, Listing 8-3 shows the Check method that implements the actual rule and the AttributeType method to get the type for the AssembliesHaveCopyrightAttributesRule. Because all the assembly attributes rules I wrote (AssembliesHaveCompanyAttributesRule, AssembliesHaveCopyrightAttributesRule, AssembliesHaveDescriptionAttributesRule, and AssembliesHaveTitleAttributesRule) are almost identical, they all share a base class, BaseAssemblyAttributeRule, and they simply define the abstract AttributeType property to return the particular method to look up. You can see all the action in full in the Wintellect.FxCop.DesignRules project.

Listing 8-3. Checking if an assembly attribute exists

[View full width]

/// <summary> /// The property all derived classes use to return the attribute type /// they are looking for. /// </summary> abstract public TypeNode AttributeType { get; } /// <summary> /// Does the actual rule validation. /// </summary> /// <param name="module"> /// The assembly to check for a specific attribute. /// </param> /// <returns> /// null means the attribute is present. /// </returns> /// <exception cref="ArgumentNullException"> /// Thrown if <paramref name="module"/> is null or Nothing. /// </exception> public override ProblemCollection Check ( Module module ) { if ( null == module ) { throw new ArgumentNullException ( Constants.InvalidModule ); } // Is this module an assembly? if ( NodeType.Assembly != module.NodeType ) { return ( null ); } // Get the custom attribute type. AttributeList attribList = RuleUtilities.GetCustomAttributes ( module , AttributeType ); if ( 1 == attribList.Length ) { // Get the value of the attribute because it does exist. Since // I've found one attribute, that means there's an // ExpressionList with one item in it. Converting that to a // string, we get the value of the attribute. I figured this out // by looking at all the attributes on a module and looking at how // their values were determined. Thank goodness for the Watch // window when it comes to some serious spelunking! String strValue = attribList [ 0 ] .Expressions [ 0 ]. ToString ( ); if ( 0 == strValue.Length ) { // Build up the error string to return. Problem prob = CreateModuleProblem ( module.Name ); Problems.Add ( prob ); } } else { // Build up the error string to return. Problem prob = CreateModuleProblem ( module.Name ); Problems.Add ( prob ); } return ( Problems ); }



Before jumping into the advanced rule development, I want to point out two hints that will help speed your rule development. The first is that you should definitely spend some quality time in Reflector looking at the Microsoft FxCop.Sdk.Introspection.RuleUtilities so you can see more examples of using the Microsoft.Cci classes in your own rules.

The second hint is about debugging your rules. If you're building your rules to use the stand-alone FxCop program, you need just to set the Start action in the Debug property tab to Start External Program and set the program to FxCop.exe. Whenever you load your rule into FxCop.exe, your breakpoints will revert from unresolved to resolved, and you'll be able to debug as you would any normal class library.

If you are building your rules to use the internal Visual Studio 2005 Code Analysis version, it gets a little more interesting to do your debugging. The actual analysis is not done inside Visual Studio 2005, it's done in a program called FxCopCmd.exe located in the Visual_Studio_2005_Install_Directory\Team Tools\Static Analysis Tools\FxCop directory. If you were super fast on the keyboard, you could try to attach to the FxCopCmd.exe process when it starts, but you'd probably miss it. You could also add a Debugger.Break call inside your rule, but the odds are quite high that you'd accidentally leave that call in when you check your code in. Fortunately, you can specify command-line parameters to FxCopCmd.exe to specify the module to analyze and the rule assembly you want to run.

After you set the Start action in the Debug property tab to Start External Program and the program to FxCopCmd.exe, you'll need to set the command-line options. You have to specify /console to indicate that you want error output to go to the screen. The /file: option allows you to specify the assembly to analyze, and /rule: is what you use to set the rule assembly to use. Now you can easily debug your Code Analysis rules. If this sounds tedious to set repeatedly, look at the FxCopCmdDebug.SettingsMaster file in the Wintellect.FxCop.UsageRules directory. You can use that file with the Settings Master add-in presented in Chapter 7, "Extending the Visual Studio IDE," to update all configurations to debug a specific program repeatedly.




Debugging Microsoft  .NET 2.0 Applications
Debugging Microsoft .NET 2.0 Applications
ISBN: 0735622027
EAN: 2147483647
Year: 2006
Pages: 99
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net