As promised, this is the first installment of a couple posts about writing custom rules for FxCop. This sample was written for FxCop 1.312, so if you have a different version, I can't guarantee that this method will work.
The expected outline of this walkthrough/post and a follow-up post should look like this:
Core Examples - Overview of the sample rule(s) I'm using to help explain the process
- How to define the rule in the embedded xml file
- Description of the RuleUtilities class and some of it's members
- Describing and extending the BaseIntrospectionRule
- Description and example of the Check methods
- Example rules (code)
Tips/tricks - How to debug your rule
- A program used to read rule definitions and apply them to a template file. - I use this to generate FlexWiki pages for my rules. It makes for a convenient way for any developer to update/modify the rule examples, details, etc.
When I wrote my first rule, I found a couple resources that helped with the process. There is a MSDN
bugslayer article for writing rules. John
updated the code to work with FxCop 1.312. Also,
Brady has a
sample rule that he posted. As always,
Reflector was very useful for determining what could be done with introspection. Anyway, on with the article...
Overview The rules that I will use for this walkthrough will check the method used to raise an event. The method name should start with 'On' as in OnClick. It should be a protected method and only have one parameter (of type System.EventArgs). This follows Microsoft's naming guidelines for events...
Rule Definitions FxCop uses an embedded xml file to describe each rule. This information is then plugged into the FxCop gui so that the user knows what is wrong. I have yet to find a schema for this xml doc, but for these tasks, my xml doc looks like this:
<?xml version="1.0" encoding="utf-8" ?> <Rules> <Rule TypeName="MethodUsedToRaiseEventShouldStartWithOn" Category="BiaCreations.Naming" CheckId="Bia1001"> <Name>Method Used To Raise Event Should Start With On</Name> <Description>Events should be associated with a method prefixed with 'On' to raise the event</Description> <Url>http://127.0.0.1/DevWiki/default.aspx/MethodUsedToRaiseEventShouldStartWithOn</Url> <Resolution>Create a protected method to raise the event. The method should be named 'On{0}'</Resolution> <Email>jim@NO_SPAMbiacreations.com</Email> <MessageLevel Certainty="95">Error</MessageLevel> <FixCategories>Breaking</FixCategories> <Owner>Jim Geurts</Owner> <GroupOwner>BiaCreations</GroupOwner> <DevOwner>Jim Geurts</DevOwner> </Rule> <Rule TypeName="MethodUsedToRaiseEventShouldOnlyHaveOneParameter" Category="BiaCreations.Naming" CheckId="Bia1002"> <Name>Method Used To Raise Event Should Only Have One Parameter</Name> <Description>Methods used to raise events should only have one parameter, of type System.EventArgs</Description> <Url>http://127.0.0.1/DevWiki/default.aspx/MethodUsedToRaiseEventShouldOnlyHaveOneParameter</Url> <Resolution>Modify the parameters for the method so that there is only one System.EventArgs parameter.</Resolution> <Email>jim@NO_SPAMbiacreations.com</Email> <MessageLevel Certainty="95">Error</MessageLevel> <FixCategories>Breaking</FixCategories> <Owner>Jim Geurts</Owner> <GroupOwner>BiaCreations</GroupOwner> <DevOwner>Jim Geurts</DevOwner> </Rule> <Rule TypeName="UseProtectedMethodToRaiseEvent" Category="BiaCreations.Naming" CheckId="Bia1003"> <Name>Method Used To Raise Event Should Be Protected</Name> <Description>Methods used to raise events should have an access level of protected.</Description> <Url>http://127.0.0.1/DevWiki/default.aspx/UseProtectedMethodToRaiseEvent</Url> <Resolution>Modify the access level of the method to be protected.</Resolution> <Email>jim@NO_SPAMbiacreations.com</Email> <MessageLevel Certainty="95">Error</MessageLevel> <FixCategories>Breaking</FixCategories> <Owner>Jim Geurts</Owner> <GroupOwner>BiaCreations</GroupOwner> <DevOwner>Jim Geurts</DevOwner> </Rule></Rules>The <MessageLevel> element allows these values: CriticalError, Error, CriticalWarning, Warning, and Informational. The <FixCategories> element allows these values: Breaking, NonBreaking, and Unknown. The <resolution> element allows you to put string formatting markers ({0}, {1}, etc) which get replaced by FxCop. I will touch on where you specify the replacements a little later.
RuleUtilities Class The RuleUtilities class provides helper methods to explore/discover various objects. For example, it has a method that attempts to get a Method object from a member. It also gives you access to the spell checking api, if that's your type of thing. To find out if the member is a method, you can use the RuleUtilities library like this:
Method method = RuleUtilities.GetMethod(member);if (method != null){ // The member is a method. // TODO: Test the method somehow}Say you're scanning an object and you want to get all members from that object's class. This should do the trick:
MemberList memberList = RuleUtilities.GetAllMembersOnType(member.DeclaringType);
If you're looking to check the spelling of a string, give this a try:
SpellChecker spellChecker = RuleUtilities.GetSpellChecker(); SpellerStatus spellCheckResult = spellChecker.CheckWord(someWord); // TODO: Handle each result
There are countless other uses for the RuleUtilities, so just go play around with it. It's a great utility class, even though there is no documentation.
Extending BaseIntrospectionRule I chose to create an abstract base class for all of my rules. It extends BaseIntrospectionRule... Beside having a common class to create problems and store common methods, it is also nice not to have to define all 3 parameters of the BaseIntrospectionRule constructor for each rule. My base class looks like this:
//// Author: James Geurtsusing System;using System.IO;using Microsoft.Cci;using Microsoft.Tools.FxCop.Sdk;using Microsoft.Tools.FxCop.Sdk.Introspection;namespace BiaCreations.FxCop.Rules{ /// <summary> /// Summary description for IntrospectionRuleBase. /// </summary> public abstract class IntrospectionRuleBase : BaseIntrospectionRule { /// <summary> /// Creates a new <see cref="IntrospectionRuleBase"/> instance. /// </summary> /// <param name="ruleName">Name of the rule</param> protected IntrospectionRuleBase(string ruleName) : base(ruleName, "BiaCreations.FxCop.Rules.RuleDefinitions", typeof(IntrospectionRuleBase).Assembly) { } /// <summary> /// Gets the target visibility. /// </summary> public override TargetVisibilities TargetVisibility { get { return (TargetVisibilities.Overridable | (TargetVisibilities.NotExternallyVisible | TargetVisibilities.ExternallyVisible)); } } /// <summary> /// Helper function to build up the problem error. /// </summary> /// <param name="moduleName">The name of the module.</param> /// <returns>The allocated problem. </returns> protected Problem CreateModuleProblem( String moduleName ) { String fileName = Path.GetFileName ( moduleName ) ; return CreateProblem(fileName); } /// <summary> /// Helper function to build up the problem error. /// </summary> /// <param name="typeName">The name of the type</param> /// <returns>The allocated problem. </returns> protected Problem CreateTypeProblem(string typeName) { return CreateProblem(typeName); } /// <summary> /// Helper function to build up the problem error. /// </summary> /// <param name="args">Arguments to pass to the resolution</param> /// <returns>The allocated problem. </returns> protected Problem CreateProblem(params string[] args) { Resolution res = GetResolution (args) ; return new Problem(res); } } }The two methods CreateModuleProblem and CreateTypeProblem just wrap the CreateProblem method. I could have just used CreateProblem throughout my rules, but this way it's a little more descriptive.
Check(...) Methods The BaseIntrospectionRule class provides some overloaded versions of the Check(...) method. The available methods are:
Check(Member member)
- Useful for checking members of a class. (Methods, Properties, Events, Delegates, etc)
Check(Module module)
- Useful for checking assemblies.
Check(Parameter parameter)
- Checks all parameters
Check(Resource resource)
- Checks all resources (from .resx files)
Check(TypeNode type)
- Checks types
Check(string namespaceName, TypeNodeList types)
- Checks types
So when FxCop scans an assembly, it will come across an object - say a method. FxCop then goes through all of the loaded rules and executes Check(Member member) for the method. At that point, the rule can generate a new Problem or do nothing at all. The constructor for Problem takes a Resolution object. So when you create a problem, you pass the [string] parameters to be used with the resolution. These parameters replace the {0}, {1}, etc in the resolution defined in the embedded xml file that defines the rule.
Example Rules Finally, I get to the examples...
The following rule will make sure that there is a method that starts with 'On' to raise the event. For each event that is processed, it will scan that event's class file for a method named OnEventName. So if it finds an event named 'Click', it will report a Problem if there is no 'OnClick' method present.
// Filename: MethodUsedToRaiseEventShouldStartWithOn.cs//// Author: James Geurtsusing System;using Microsoft.Cci;using Microsoft.Tools.FxCop.Sdk;using Microsoft.Tools.FxCop.Sdk.Introspection;namespace BiaCreations.FxCop.Rules.Naming{ /// <summary> /// Summary description for MethodUsedToRaiseEventShouldStartWithOn. /// </summary> public class MethodUsedToRaiseEventShouldStartWithOn : IntrospectionRuleBase { /// <summary> /// Creates a new <see cref="MethodUsedToRaiseEventShouldStartWithOn"/> instance. /// </summary> public MethodUsedToRaiseEventShouldStartWithOn() : base("MethodUsedToRaiseEventShouldStartWithOn") { } /// <summary> /// Checks the specified member. /// </summary> /// <param name="member">Member to check</param> /// <returns></returns> public override ProblemCollection Check(Member member) { // Ignore the member if it isn't an event if (member.NodeType != NodeType.Event) { return base.Check(member); } // If a method exists with the 'On' prefixing the name of the event, assume that it is the correct method // to raise the event. MemberList memberList = RuleUtilities.GetAllMembersOnType(member.DeclaringType); for (int i = 0; i < memberList.Length; i++) { Method method = RuleUtilities.GetMethod(memberList[i]); if (method != null) { if (method.Name.Name == "On" + member.Name.Name) { return base.Check(member); } } } Problem problem = CreateProblem(member.Name.Name); base.Problems.Add(problem); return base.Problems; } }}The next rule will make sure that the method used to raise an event only has 1 parameter of type System.EventArgs. Again, the name of the method must be called OnEventName. For a 'Click' event, the method name would be 'OnClick'
// Filename: MethodUsedToRaiseEventShouldOnlyHaveOneParameter.cs//// Author: James Geurtsusing System;using System.Collections;using Microsoft.Cci;using Microsoft.Tools.FxCop.Sdk;using Microsoft.Tools.FxCop.Sdk.Introspection;namespace BiaCreations.FxCop.Rules.Naming{ /// <summary> /// Summary description for MethodUsedToRaiseEventShouldOnlyHaveOneParameter. /// </summary> public class MethodUsedToRaiseEventShouldOnlyHaveOneParameter : IntrospectionRuleBase { /// <summary> /// Creates a new <see cref="MethodUsedToRaiseEventShouldOnlyHaveOneParameter"/> instance. /// </summary> public MethodUsedToRaiseEventShouldOnlyHaveOneParameter() : base("MethodUsedToRaiseEventShouldOnlyHaveOneParameter") { } /// <summary> /// Checks the specified member only has one parameter (of type EventArgs) /// </summary> /// <param name="member">Member to check</param> /// <returns></returns> public override ProblemCollection Check(Member member) { // Ignore the member if it isn't an event if (member.NodeType != NodeType.Event) { return base.Check(member); } // If a method exists with the 'On' prefixing the name of the event, assume that it is the correct method // to raise the event. So check to make sure that it only has one parameter of base type System.EventArgs MemberList memberList = RuleUtilities.GetAllMembersOnType(member.DeclaringType); bool containsRaiseMethod = false; for (int i = 0; i < memberList.Length; i++) { Method method = RuleUtilities.GetMethod(memberList[i]); if (method != null) { if (method.Name.Name == "On" + member.Name.Name) { if (method.Parameters.Length == 1) { Parameter param = method.Parameters[0]; if (param.Type == SystemTypes.EventArgs) { return base.Check(member); } } containsRaiseMethod = true; } } } // Only fire a problem for methods that exist and do not match the expected format if (containsRaiseMethod) { Problem problem = CreateProblem("On" + member.Name.Name); base.Problems.Add(problem); return base.Problems; } return base.Check(member); } }}Finally, the last rule will make sure that the method used to raise an event only is a protected method. Again, the name of the method must be called OnEventName. For a 'Click' event, the method name would be 'OnClick'
// Filename: UseProtectedMethodToRaiseEvent.cs//// Author: James Geurtsusing System;using Microsoft.Cci;using Microsoft.Tools.FxCop.Sdk;using Microsoft.Tools.FxCop.Sdk.Introspection;namespace BiaCreations.FxCop.Rules.Naming{ /// <summary> /// Summary description for UseProtectedMethodToRaiseEvent. /// </summary> public class UseProtectedMethodToRaiseEvent : IntrospectionRuleBase { /// <summary> /// Creates a new <see cref="UseProtectedMethodToRaiseEvent"/> instance. /// </summary> public UseProtectedMethodToRaiseEvent() : base("UseProtectedMethodToRaiseEvent") { } /// <summary> /// Checks the specified member only has one parameter (of type EventArgs) /// </summary> /// <param name="member">Member to check</param> /// <returns></returns> public override ProblemCollection Check(Member member) { // Ignore the member if it isn't an event if (member.NodeType != NodeType.Event) { return base.Check(member); } // If a method exists with the 'On' prefixing the name of the event, assume that it is the correct method // to raise the event. So check to make sure that it is a protected method. MemberList memberList = RuleUtilities.GetAllMembersOnType(member.DeclaringType); bool containsRaiseMethod = false; for (int i = 0; i < memberList.Length; i++) { Method method = RuleUtilities.GetMethod(memberList[i]); if (method != null) { if (method.Name.Name == "On" + member.Name.Name) { // If the method is protected, we're all good if ((method.IsFamily || method.IsFamilyOrAssembly) || method.IsFamilyAndAssembly) { return base.Check(member); } containsRaiseMethod = true; } } } // Only fire a problem for methods that exist and do not match the expected format if (containsRaiseMethod) { Problem problem = CreateProblem("On" + member.Name.Name); base.Problems.Add(problem); return base.Problems; } return base.Check(member); } }}
Related...
http://blogs.biasecurities.com/jim/archive/2004/12/29/818.aspx | Comments