Writing FxCop Globalization Rules


Now that we know what all of the new rules are, we can turn our attention to implementing them. In earlier versions, FxCop used an analysis engine that was based upon reflection. Since v1.30, FxCop has supported a new engine that is based upon introspection, and the reflection engine has now been dropped. All of the rules in this section are based on the introspection engine. Introspection is a reflection-like technology that is multithreaded, provides faster analysis of assemblies, works with different versions of the .NET Framework, and, unlike reflection, does not lock those assemblies.

Getting Started Writing FxCop Rules

As I mentioned earlier, we look at the rules in order of their categories. We start our journey with a resource rulethat is, a rule that analyzes resources. This rule is "top heavy" because we use it as an example of how to write FxCop rules in general.

FxCop introspection rules are .NET classes that inherit from BaseIntrospection Rule. They are placed in a class library and must have an associated entry in an embedded XML resource. Start by creating a new class library. I called my project and solution "GlobalizationRules", but to avoid a clash with FxCop's own DLLs, I set the output assembly to "I18NBook.GlobalizationRules". Add references to FxCopSdk.dll and Microsoft.Cci.dll (both are in the FxCop folder). We will create an abstract globalization rule base class from which all of our globalization rules will inherit. This gives us a point at which we can add more functionality later:

 public abstract class BaseGlobalizationRule: BaseIntrospectionRule {     protected BaseGlobalizationRule(string name):         base(name, "GlobalizationRules.RuleData",         typeof(BaseGlobalizationRule).Assembly)     {     } } 


The constructor accepts a string that represents the rule's name. This string is critical to binding the rule to its rule data, as we shall see in the first rule. The BaseGlobalizationRule's constructor calls the BaseIntrospectionRule's constructor and passes the rule name, a fully qualified XML resource name, and an assembly where the fully qualified XML resource can be found. The XML resource is an XML file that contains essential property values of each rule. Because we will use the same XML file for all of our rules, we can specify this here in the base class and not bother our descendants with this problem or the problem of where this XML resource can be found. We will see an implementation of the XML resource in the first rule.

Add a new XML file to the project (in Solution Explorer, right-click the Global-izationRules project; select Add, New Item...; and select XML File). Name the file "RuleData.xml". The name of the file is important. The second parameter passed to the BaseIntrospectionRule constructor must identify this XML resource by name. If the file is called "RuleData.xml" and the project's default namespace is "GlobalizationRules", the fully qualified name of the RuleData resource is "GlobalizationRules.RuleData". Finally, in the Properties Window of Rule-Data.xml, set Build Action to Embedded Resource so that the RuleData.xml file is embedded into the GlobalizationRules assembly as a resource.

"Control characters embedded in resource string"

To create a rule, we need to write a new class and inherit from our BaseGlobalizationRule:

 public class ControlCharactersEmbeddedInResourceString:     BaseGlobalizationRule {     public ControlCharactersEmbeddedInResourceString():        base("ControlCharactersEmbeddedInResourceString")     {     } } 


The constructor calls the BaseGlobalizationRules constructor and passes the name of the rule. This name must exactly match the entry in the XML rules file. This brings us to adding the entry in the XML rules file. The XML rules file is a simple XML file with a <Rules> element containing zero or more <Rule> tags. Each <Rule> must have an attribute called TypeName that exactly matches the string passed to the BaseGlobalizationRule constructor. The Rule elements provide more information about the rule. Here's an XML document, including the definition of the ControlCharactersEmbeddedInResourceString rule:

 <?xml version="1.0" encoding="utf-8" ?> <Rules>    <Rule TypeName="ControlCharactersEmbeddedInResourceString"    Category="Globalization" Check>       <Name>Control characters embedded in resource string       </Name>       <Description>Resource string has control characters embedded in       it. Strings should not contain control characters because they       represent functionality and not text       </Description>       <Owner>Guy Smith-Ferrier</Owner>       <Url>http://www.dotnet18n.com/fxcop</Url>       <Resolution>Break up the string in "{0}" into separate strings       and use Environment properties instead of control characters or       hard code the control character strings. The string is "{1}".       </Resolution>       <Email></Email>       <MessageLevel Certainty="95">Warning</MessageLevel>       <FixCategories>NonBreaking</FixCategories>    </Rule> </Rules> 


The Name element is shown in the error list. If you are using the stand-alone FxCop GUI, you can see most of the other elements in the Rule Details tab of the Message Details dialog (see Figure 13.8).

Figure 13.8. Rule Details Tab Showing the Use of A Rule's XML Information


Perhaps the most important element is the Resolution, which, in this rule, is:

 Break up the string in "{0}" into separate strings and use Environment properties instead of control characters or hard code the control character strings. The string is "{1}". 


It is important because it is shown in the Issues tab of the Message Details dialog, which is the first thing the developer sees when double-clicking the error (see Figure 13.9).

Figure 13.9. Message Details Showing the Rule's Resolution


What is important about it is that it contains information about the specific error in question. In the Resolution element, you can see that it has two placeholders: {0} and {1}. In the Issues tab, you can see that these placeholders have been substituted with values "FxCopAdditionalGlobalizationRulesTest.Form1 Resources.resources" and "You have 5 deliveries\nPress any key". When we implement the rule, you will see how these placeholders are filled.

Visual Studio 2003 has an excellent gotcha that you should be aware of when modifying the XML rules file. If you modify the XML file, the change is not considered to be important enough to rebuild the XML resource (even if you save the XML file after making your change). This means that you can add new rules to the XML rules file and build your rules assembly, only to find that FxCop doesn't see your new or modified rules. To get the change noticed, you must do a rebuild (Build, Rebuild Solution). This behavior is the same for all embedded resources.


To implement the rule, you override one of the BaseIntrospectionRule.Check methods:

 public virtual ProblemCollection Check(Member member); public virtual ProblemCollection Check(Module module); public virtual ProblemCollection Check(Parameter parameter); public virtual ProblemCollection Check(Resource resource); public virtual ProblemCollection Check(TypeNode type); public virtual ProblemCollection Check(string namespaceName,     TypeNodeList types); 


The strategy is to look through the list of possibilities, find the one that matches the entity that you want to analyze, and override that method. In our case, we want to analyze resources to see if they contain control characters, so we override the Check(Resource) method. Here's our implementation:

 public override ProblemCollection Check(Resource resource) {     ResourceReader reader = new ResourceReader(         new System.IO.MemoryStream(resource.Data));     try     {         IDictionaryEnumerator enumerator = reader.GetEnumerator();         while (enumerator.MoveNext())         {             if (ResourceStringHasEmbeddedControlCharacters(                 enumerator.Value))             {                 Resolution resolution = GetResolution(                     new string[] {resource.Name.ToString(),                     enumerator.Value.ToString()});                 Problems.Add(new Problem(resolution));                 return Problems;             }         }     }     finally     {         reader.Close();     }     return base.Check(resource); } 


Our method is called iteratively for every resource in the fallback assembly. The resource is loaded from resource.Data using a MemoryStream. We read through each item in those resources using a ResourceReader. For each item, we call ResourceStringHasEmbeddedControlCharacters, which is implemented like this:

 protected virtual bool ResourceStringHasEmbeddedControlCharacters(     object resourceObject) {     if (resourceObject is string)     {         string resourceString = (string) resourceObject;         int startIndex = 0;         int foundCharacter;         while ((foundCharacter =             resourceString.IndexOf('\\', startIndex)) > -1)         {             if (foundCharacter < resourceString.Length &&                 resourceString[foundCharacter + 1] != '\\')                 return true;             startIndex = foundCharacter + 1;         }     }     return false; } 


This test checks that the value is a string and that it contains a "\", which is not the last character and is not followed by another "\" (two "\" characters indicates a literal "\" and not a control character). If the string is identified to contain a control character, we need to report this problem back to FxCop. We do this by creating a new Problem object, adding it to the Problems collection, and returning it from the method:

 Resolution resolution = GetResolution(     new string[] {resource.Name.ToString(),     enumerator.Value.ToString()}); Problems.Add(new Problem(resolution)); return Problems; 


GeTResolution is a BaseIntrospectionRule method that gets the Resolution element of the rule as specified in our RuleData.xml file. Recall that our resolution string has two placeholders. These placeholders are replaed by the two parameters passed to GeTResolution, which are the name of the resource and the value of the resource string. Finally, the resolution is converted to an array of Problem objects. Our rule is complete.

Type/Resource Rules

"Form.Language must be (Default)"

This rule represents the first of our Type/Resource rules. These are similar to the Resource rule that we have just looked at, but they allow us to iterate through resources of a given type. In the case of the "Form.Language must be (Default)" rule, the type is "System.Windows.Forms.Form". Because this functionality is shared across several rules, I have written a new base class, BaseTypeResourceRule, to encapsulate this functionality:

 public abstract class BaseTypeResourceRule: BaseGlobalizationRule {     private string typeName;     public BaseTypeResourceRule(string name, string typeName):         base(name)     {         this.typeName = typeName;     }     public override ProblemCollection Check(TypeNode type)     {         if (TypeIsSubClassOf(type, typeName))         {             Resource resource;             if (GetResource(type, out resource))                 return CheckResource(type, resource);         }         return base.Check(type);     }     public abstract ProblemCollection CheckResource(         TypeNode type, Resource resource);     protected virtual void ResourceToList(         Resource resource, IList list)     {         ResourceReader reader = new ResourceReader(             new MemoryStream(resource.Data));         try         {             IDictionaryEnumerator enumerator =                 reader.GetEnumerator();             while (enumerator.MoveNext())             {                 list.Add(new DictionaryEntry(                     enumerator.Key, enumerator.Value));             }         }         finally         {             reader.Close();         }     } } 


The BaseTypeResourceRule overrides the Check(TypeNode) method and checks that the type matches the type specified by the subclass ("System.Windows.Forms.Form", in our example). If the type matches, it calls the BaseGlobalizationRule.GetResource() method to get the resource that corresponds to the type (if any). It then calls BaseTypeResourceRule.CheckResource() to check the resource. This is the method that the subclass overrides to implement its own rule. (I chose to call this method CheckResource instead of Check to avoid clashing with the existing Check(Resource) method signature.)

The ResourceToList method is a helper method that exists to read the list of resource entries into an IList. This method is useful when you need bidirectional navigation through the list.

The FormLanguageMustBeDefault rule is implemented like this:

 public class FormLanguageMustBeDefault: BaseTypeResourceRule {     public FormLanguageMustBeDefault(): base(         "FormLanguageMustBeDefault",         "System.Windows.Forms.Form")     {     }     public override ProblemCollection CheckResource(         TypeNode type, Resource resource)     {         ResourceReader reader = new ResourceReader(             new System.IO.MemoryStream(resource.Data));         try         {             IDictionaryEnumerator enumerator =                 reader.GetEnumerator();             while (enumerator.MoveNext())             {                 string key = (string) enumerator.Key;                 if (key == "$this.Language")                 {                     if (enumerator.Value != null &&                         enumerator.Value is CultureInfo &&                         ((CultureInfo) enumerator.Value).                         Equals(CultureInfo.InvariantCulture))                         return null;                     else                     {                         Resolution resolution = GetResolution(                             new string[] {type.Name.Name});                         Problems.Add(new Problem(resolution));                         return Problems;                     }                 }             }             return null;         }         finally         {             reader.Close();         }     } } 


The constructor ensures that the CheckResource method is only ever called for resources of System.Windows.Forms.Form. We override the CheckResource method and iterate through all of the items looking for the "$this.Language" entry. We cast the corresponding value to a CultureInfo and check that it is CultureInfo.InvariantCulture (i.e., "(Default)"). If it isn't, we return a problem.

"Form.Localizable must be true"

This rule is almost a carbon copy of the last rule. The difference is that we are looking for an entry that is "$this.Localizable", and we are checking that it is true. Recall, however, from the earlier explanation of this rule that this rule does not work with forms developed for the .NET Framework 2.0, and this is because the implementation of the Form.Localizable resource changed from being data to metadata. In the previous CheckResource method, the ResourceReader reads only the resource data and not the metadata:

 ResourceReader reader = new ResourceReader(     new System.IO.MemoryStream(resource.Data)); 


At such time as FxCop includes metadata in its Resource class, this line could be modified for analyzing .NET Framework 2.0 forms.

"Label.AutoSize must be true"

This rule needs to be slightly smarter than the previous two Type/Resource rules. It needs to look for controls that are of type "System.Windows.Forms.Label" and then check their corresponding AutoSize property (if any). When writing a rule like this, it helps if you take a look at the resx file from which the resource is compiled. Add a label to a form and open the form's resx file with a text editor. Look for an entry similar to the following:

 <data name="&gt;&gt;label1.Type">   <value>System.Windows.Forms.Label, System.Windows.Forms,     Version=1.0.5000.0, Culture=neutral,     PublicKeyToken=b77a5c561934e089   </value> </data> 


This is the type definition of the label. We need to find these entries to identify which controls are Label controls. "&gt;&gt;" is the way that ">>" is represented in XML, so the name attribute in this example is ">>label1.Type". Consequently, we are looking for entries that start with ">>" and end with ".Type", and whose value element begins with "System.Windows.Forms.Label, ". Notice that I don't check the remainder of the type definition because I don't want the rule to be limited to a specific version of the .NET Framework.

Having found a type definition for a label, we can extract the label's name (i.e., "label1"). From this, we can look for the AutoSize property, which, in the resx file, will be:

 <data name="label1.AutoSize" type="System.Boolean, mscorlib,   Version=1.0.5000.0, Culture=neutral,   PublicKeyToken=b77a5c561934e089">   <value>False</value> </data> 


From here, we need only check that the value element is true. This kind of checking requires us to have bidirectional access to the entries in the resource. As such, we will make use of the ResourceToList method implemented in our BaseTypeResourceRule class. With our strategy in place, we can look at the implementation of our CheckResource method:

 public override ProblemCollection CheckResource(     TypeNode type, Resource resource) {     StringCollection badControls = new StringCollection();     ArrayList list = new ArrayList();     ResourceToList(resource, list);     foreach(DictionaryEntry dictionaryEntry in list)     {         string key = (string) dictionaryEntry.Key;         if (key.StartsWith(">>") && key.EndsWith(".Type"))         {             string entryType = (String) dictionaryEntry.Value;             if (entryType.StartsWith("System.Windows.Forms.Label, "))             {                 // This is a label. Now check its AutoSize property.                 // If the key is ">>label1.Type" then the control                 // name is "label1".                 string controlName = key.Substring(2, key.Length -7);                 int labelAutoSizeIndex = IndexOfDictionaryEntry(                     list, controlName + ".AutoSize");                 if (labelAutoSizeIndex == -1 ||                     ! (bool) ((DictionaryEntry)                     list[labelAutoSizeIndex]).Value)                     badControls.Add(controlName);             }         }     }     if (badControls.Count == 0)         return null;     else     {         StringBuilder badControlNames =             new StringBuilder(badControls[0]);         for(int badControlNumber = 1;             badControlNumber < badControls.Count; badControlNumber++)         {             badControlNames.Append(                 ", " + badControls[badControlNumber]);         }         Resolution resolution = GetResolution(new string[]             {type.Name.Name, badControlNames.ToString()});         Problems.Add(new Problem(resolution));         return Problems;     } } 


You can see from this method that when a problem is found, it is not reported immediately. Instead, a list of offending controls is built up, and when the check is complete, the list is converted to a single string.

Instruction Rules

"DateTime.ToString() should not use a culture specific format"

The "DateTime.ToString() should not use a culture specific format" rule is the first of the instruction rules that we will look at. These rules work by looking at the IL instructions of the code in the assembly. We override the Check(Member) method and iterate over the instructions of every method, looking for a given pattern. This requires you to appreciate the problem from the perspective of IL instead of your chosen development language, but if you aren't intimate with IL, this isn't quite as difficult as it may seem. The skeleton of the Check method is:

 public override ProblemCollection Check(Member member) {     Method method = member as Method;     if (method == null)         return null;     for(int instructionNumber = 1; instructionNumber <         method.Instructions.Length; instructionNumber++)     {         Microsoft.Cci.Instruction instruction =             method.Instructions[instructionNumber];         // Perform some analysis on the instruction     }     return base.Check(method); } 


This code simply iterates through each instruction. In this rule, we start at the second instruction (i.e., instruction 1) because we need to look at each instruction's previous instruction, and the first instruction obviously doesn't have a previous instruction. Having obtained each instruction, the most likely action to take is to check what kind of instruction you have, and you can do this with the Instruction.OpCode property. This is where life isn't quite as difficult as it may seem. If you are wondering how to decipher the IL in an assembly without having to learn IL first, use ILDasm. In the case of our DateTime.ToString rule, the simplest solution is to write a piece of code with the offending line in it and then decompile it. Here's an offending snippet of source code:

 public virtual string GetDate() {     DateTime dateTime = new DateTime(2005, 31, 1);     return dateTime.ToString("MM/dd/yyyy"); } 


If you decompile this with ILDasm, you get:

 .method public hidebysig newslot virtual         instance string  GetDate() cil managed {   // Code size       32 (0x20)   .maxstack  4   .locals init ([0] valuetype [mscorlib]System.DateTime dateTime,            [1] string CS$00000003$00000000)   IL_0000:  ldloca.s   dateTime   IL_0002:  ldc.i4     0x7d5   IL_0007:  ldc.i4.s   31   IL_0009:  ldc.i4.1   IL_000a:  call    instance void [mscorlib]                     System.DateTime::.ctor(int32, int32, int32)   IL_000f:  ldloca.s   dateTime   IL_0011:  ldstr      "MM/dd/yyyy"   IL_0016:  call    instance string                     [mscorlib]System.DateTime::ToString(string)   IL_001b:  stloc.1   IL_001c:  br.s       IL_001e   IL_001e:  ldloc.1   IL_001f:  ret } // end of method Form1::GetDate 


The important point to grasp here is that when you iterate over the instructions in the method, they will come out in the order in which you can see them in ILDasm. Furthermore, the Instruction.OpCode will either exactly match or closely resemble the IL instruction name that you can see for each line. So, in this example, the first instruction will be a "ldloca.s" instruction and the second will be a "ldc.i4" instruction.

In the case of our DateTime.ToString rule, we are looking for the following pattern:

 IL_0011:  ldstr     "MM/dd/yyyy" IL_0016:  call      instance string [mscorlib]                     System.DateTime::ToString(string) 


The ldstr instruction loads a literal string, and the call instruction calls a method on an object. We can see the class name and method from the call instruction (System.DateTime::ToString(string)), and we can see the literal string that is loaded "MM/dd/yyyy". So our rule must look for a call to the System.DateTime. ToString method that is immediately preceded by a ldstr instruction, and check that the string is not culture specific.

Here's the implementation:

 public override ProblemCollection Check(Member member) {     Method method = member as Method;     if (method == null)         return null;     for(int instructionNumber = 1; instructionNumber <         method.Instructions.Length; instructionNumber++)     {         Microsoft.Cci.Instruction instruction =             method.Instructions[instructionNumber];         if (instruction.OpCode == OpCode.Call &&             instruction.Value is Microsoft.Cci.Method)         {             Microsoft.Cci.Method instructionMethod =                 (Microsoft.Cci.Method) instruction.Value;             if (instructionMethod.FullName ==                 "System.DateTime.ToString(System.String)")             {                 // This is a call to DateTime.ToString. Check                 // to see if the previous instruction is a ldstr.                 Microsoft.Cci.Instruction previousInstruction =                     method.Instructions[instructionNumber - 1];                 if (previousInstruction.OpCode == OpCode.Ldstr)                 {                     // This instruction is a "load string".                     string loadString =                         previousInstruction.Value.ToString();                     if (DateTimeFormatStringIsCultureSpecific(                         loadString))                     {                         Resolution resolution = GetResolution(                             new string[] {loadString});                         Problems.Add(new Problem(resolution));                         return Problems;                     }                 }             }         }     }     return base.Check(member); } 


Having identified the string parameter to DateTime.ToString(), it then calls the DateTimeFormatStringIsCultureSpecific method:

 protected virtual bool DateTimeFormatStringIsCultureSpecific(     string format) {     if (format.Length == 1)     {         string[] cultureSensitiveFormats = new string[]         {"d", "D", "f", "F", "g", "G", "m", "M", "r", "R",         "s", "t", "T", "u", "U", "y", "Y"};         foreach(string cultureSensitiveFormat in             cultureSensitiveFormats)         {             if (format == cultureSensitiveFormat)                 return false;         }     }     return true; } 


This method verifies that the format is not one of the culture-aware formats and concludes that, if it isn't, it must, therefore, be culture specific.

"Dialog culture dictated by operating system" and "Dialog culture dictated by .NET Framework"

These rules simply check for the creation (or "instance initialization," in FxCop-speak) of new objects from specific classes. They are different from previous rules, however, in that they utilize FxCop's "visit" methods. The BaseIntrospection-Rule, from which all of the rules in this chapter inherit, inherits from FxCop's StandardVisitor class.

You should be aware that although this inheritance chain is correct at the time of writing, it may change in a future release of FxCop, and these rules might need a corresponding change at that time.


StandardVisitor implements approximately 140 methods that start with the word "Visit" (e.g., "VisitMethodCall", "VisitAssignment", "VisitExpression"). Each method "visits" a node (a fragment of code) of a different type, so VisitMethodCall visits method calls. FxCop rules begin the process of visiting nodes by calling a visit method with a wide scope. For example, the rules in this section begin the visiting process by calling VisitMethod. This fires calls to numerous "visit" methods in StandardVisitor (e.g., VisitAssignment, VisitMethodCall), according to what code is in the method that has been passed to the Check method. As the implementer of a rule, you need only override the visit method or methods that interest you. In the classes in this section, we override the VisitConstruct method that is called for each invocation of a constructor. Although all of the other visit methods will also be fired as necessary, we can remain oblivious to this fact and simply focus on the instructions that are of interest to us (i.e., the construction of new objects). This approach significantly reduces the amount of code you have to write and saves us from manually iterating through every instruction in a method. The two classes are:

 public class DialogCultureDictatedByNETFramework: NewObjectRule {     public DialogCultureDictatedByNETFramework():         base("DialogCultureDictatedByNETFramework",         new string[] {"System.Windows.Forms.PrintPreviewDialog"})     {     } } public class DialogCultureDictatedByOperatingSystem: NewObjectRule {     public DialogCultureDictatedByOperatingSystem():         base("DialogCultureDictatedByOperatingSystem",         new string[]             {                 "System.Windows.Forms.OpenFileDialog",                 "System.Windows.Forms.SaveFileDialog",                 "System.Windows.Forms.FolderBrowserDialog",                 "System.Windows.Forms.FontDialog",                 "System.Windows.Forms.ColorDialog",                 "System.Windows.Forms.PrintDialog",                 "System.Windows.Forms.PageSetupDialog",                 "System.Windows.Forms.MessageBox"             })     {     } } 


As you can see, they simply pass an array of class names to the NewObjectRule base class. Here's the NewObjectRule with the all-important Check and Visit methods missing:

 public abstract class NewObjectRule: BaseGlobalizationRule {     private string[] searchClassNames;     private StringCollection foundClassNames;     public NewObjectRule(string name, string[] searchClassNames):         base(name)     {         this.searchClassNames = searchClassNames;     } } 


The Check(Member) method checks that the member is a Method and initializes the private foundClassNames field to a new StringCollection. You cannot pass your own parameters to "visit" methods, so you have to declare fields, initialize them in the Check method, set them in the "visit" method, and then check their values upon return to the Check method.

 public override ProblemCollection Check(Member member) {     Method method = member as Method;     if (method == null)         return null;     foundClassNames = new StringCollection();     VisitMethod(method);     if (foundClassNames.Count == 0)         return base.Check(member);     else     {         StringBuilder classNames =             new StringBuilder(foundClassNames[0]);         for(int classNameNumber = 1;             classNameNumber < foundClassNames.Count;             classNameNumber++)         {             classNames.Append(", ");             classNames.Append(foundClassNames[classNameNumber]);         }         foundClassNames = null;         Resolution resolution = GetResolution(new string[]             {method.Name.Name, classNames.ToString()});         Problems.Add(new Problem(resolution));         return Problems;     } } 


Notice the call to VisitMethod. This starts the whole visiting process going. We are uninterested in the majority of the nodes that get visited, with the exception of calls to instance initializers. To zero in on these instructions, we override the VisitConstruct method:

 public override Expression VisitConstruct(Construct cons) {     if (cons != null)     {         MemberBinding memberBinding =             cons.Constructor as MemberBinding;         if (memberBinding != null)         {             InstanceInitializer instanceInitializer =                 memberBinding.BoundMember as InstanceInitializer;             if (instanceInitializer != null)             {                 foreach(string searchClassName in searchClassNames)                 {                     if (instanceInitializer.DeclaringType.FullName ==                         searchClassName &&                         foundClassNames.IndexOf(searchClassName)                         == -1)                         foundClassNames.Add(searchClassName);                 }             }         }     }     return base.VisitConstruct (cons); } 


The VisitConstruct method compares the class name being used to construct a new object with the array of class names it is looking for. When a match is found that isn't already in the private foundClassNames field, it is added to the list. Upon return to the Check method, the list is turned into an FxCop Problem if the list is not empty.

"Do not use literal strings"

From the earlier description of this rule, you can guess that it has quite a lot to do. It has to identify literal strings, ensure that they are not used in situations that are known not to be localizable, ensure that they are not already in the list of ignored literal strings, and finally save the newly identified literal strings to a file that can be read by the Literal Strings Manager. Let's get started.

We need a few private fields to keep track:

 private string ignoredLiteralStringsFilename; private DataSet ignoredLiteralStringsDataSet; private ArrayList literalStrings; private StringCollection assemblyPaths; 


ignoredLiteralStringsFilename is the filename and path of the ignored literal strings file. ignoredLiteralStringsDataSet is the DataSet that gets loaded from the ignored literal strings file. literalStrings is a list of LiteralString objects representing the literal strings that we find and the methods in which they were found. assemblyPaths is a collection of all of the paths of the assemblies that get analyzed. We need this later for finding a common directory in which to put the file containing the newly identified literal strings.

Now for the executable code. First, this rule overrides the Check(Method) method. We look at what it does in pieces. The first line ensures that we perform our test only if the method is not the InitializeComponent method of a System. Windows.Forms.Form descendant:

 if ( ! (method.Name.Name == "InitializeComponent" &&     TypeIsSubClassOf(method.DeclaringType,     "System.Windows.Forms.Form"))) 


Then we iterate over all of the instructions looking for a load string instruction. We check that the string is a candidate for localizability and isn't already in our list of ignored strings:

 string loadString = instruction.Value.ToString(); if (IsSuspectString(method, loadString)) 


The IsSuspectString method checks that there is more than one character after the white space has been removed and that the string isn't in the ignored LiteralStringsDataSet. We will see where this is initialized in a moment.

 protected virtual bool IsSuspectString(     Method method, string stringFound) {     if (! StringContainsMoreThanOneCharacter(stringFound))         return false;     if (ignoredLiteralStringsDataSet != null)         return ! IsStringIgnored(method, stringFound);     return true; } 


We read the next instruction and check that it isn't one of our excluded cases using DoNotUseLiteralString's IsInstructionNewObject and IsInstruction SpecificMethodCall helper methods:

 Microsoft.Cci.Instruction nextInstruction =     method.Instructions[instructionNumber + 1]; if (! (IsInstructionNewObject(nextInstruction, "System.Exception") ||     IsInstructionNewObject(nextInstruction,     "System.Resources.ResourceManager") ||     IsInstructionNewObject(nextInstruction,     "Internationalization.Resources.ResourceManagerProvider") ||     IsInstructionSpecificMethodCall(nextInstruction,     "System.DateTime.ToString") ||     IsInstructionSpecificMethodCall(nextInstruction,     "System.Diagnostics.Debug.WriteLine") ||     IsInstructionSpecificMethodCall(nextInstruction,     "System.Diagnostics.Trace.WriteLine"))) { 


If we've gotten this far, it is a candidate for localizability, so we need to make a record of it in the literalStrings private field and report it as a problem:

 literalStrings.Add(new LiteralStringInformation(     method.DeclaringType.FullName + "." + method.Name.Name,     loadString)); Resolution resolution = GetResolution(new string[] {loadString}); Problems.Add(new Problem(resolution)); return Problems; 


Finally, we need to learn a new trick that enables us to see when an analysis run starts and stops. For this, we override the obviously named BeforeAnalysis and AfterAnalysis:

 public DoNotUseLiteralStrings(): base("DoNotUseLiteralStrings") { } public override void BeforeAnalysis() {     assemblyPaths = new StringCollection();     literalStrings = new ArrayList();     InitializeIgnoredLiteralStringsDataSet();     base.BeforeAnalysis(); } public override void AfterAnalysis() {     base.AfterAnalysis();     WriteLiteralStrings(); } 


You can see here that instead of initializing our information about the ignored literal strings in the constructor, we wait until BeforeAnalysis is called. The strings are written back only after the analysis is complete. The only part that is missing is how to get the names of the assemblies that are being analyzed. We can get these from overriding the Check(Module) method:

 public override ProblemCollection Check(Module module) {     string path =         module.ContainingAssembly.Directory.ToString().ToUpper();     if (assemblyPaths.IndexOf(path) == -1)         assemblyPaths.Add(path);     return base.Check (module); } 


When the analysis is complete, we write out the list of literal strings that we carefully gathered into our literalStrings list to a new "LiteralStrings.xml" file ready for the Literal Strings Manager to process it.

"CultureInfo not provided by Provider", "ResourceManager not provided by provider", and "Thread not provided by ThreadFactory"

These simple rules inherit from the abstract ObjectNotProvidedByProvider class, which overrides the Check(Member) method and looks for the creation of objects that inherit from a given class:

  • "CultureInfo not provided by Provider" looks for the System.Globalization.CultureInfo class

  • "ResourceManager not provided by provider" looks for the System.Resource.ResourceManager class

  • "Thread not provided by THReadFactory" looks for the System.Threading.Thread class

It ensures that all methods of the Globalization.CultureInfoProvider, Internationalization.Resources.ResourceManagerProvider, or Internationalization.Common.ThreadFactory classes are ignored, as these methods are the only code that can legitimately create new CultureInfo, ResourceManager, or THRead objects. Like the NewObjectRule shown earlier, the ObjectNotProvidedByProvider abstract class looks for the creation of new objects by overriding the VisitConstruct method. Here's the ObjectNotProvidedByProvider class with the all-important Check and Visit methods missing:

 public abstract class ObjectNotProvidedByProvider:     BaseGlobalizationRule {     private string className;     private string providerClassName;     private bool classFound;     public ObjectNotProvidedByProvider(string name,         string className, string providerClassName): base(name)     {         this.className = className;         this.providerClassName = providerClassName;     } } 


The ObjectNotProvidedByProvider class has a Boolean field called class-Found that is initialized to false by the Check method and potentially set to true by the VisitConstruct method:

 public override ProblemCollection Check(Member member) {     Method method = member as Method;     if (method == null)         return null;     if (! TypeIsSubClassOf(method.DeclaringType, providerClassName))     {         classFound = false;         VisitMethod(method);         if (classFound)         {             Resolution resolution = GetResolution(                 new string[] {method.Name.Name});             Problems.Add(new Problem(resolution));             return Problems;         }     }     return base.Check(member); } public override Expression VisitConstruct(Construct cons) {     if (cons != null)     {         MemberBinding memberBinding =             cons.Constructor as MemberBinding;         if (memberBinding != null)         {             InstanceInitializer instanceInitializer =                 memberBinding.BoundMember as InstanceInitializer;             if (instanceInitializer != null &&                 instanceInitializer.DeclaringType.FullName                 == className)                 classFound = true;         }     }     return base.VisitConstruct (cons); } 


The "CultureInfo not provided by Provider" class inherits from ObjectNotProvidedByProvider and is representative of all three rules:

 public class CultureInfoNotProvidedByProvider:     ObjectNotProvidedByProvider {     public CultureInfoNotProvidedByProvider(): base(         "CultureInfoNotProvidedByProvider",         "System.Globalization.CultureInfo",         "Globalization.CultureInfoProvider")     {     } } 


"Resource string missing from fallback assembly"

This rule looks for instructions that load a string, sees if the string is used by ResourceManager.GetString, and ensures that the string key is in the fallback resource that corresponds to the type in which the string was found.

The rule overrides the Check(Member) method. The check for load string instructions should be getting obvious by now:

 for(int instructionNumber = 0; instructionNumber <     method.Instructions.Length; instructionNumber++) {     Microsoft.Cci.Instruction instruction =         method.Instructions[instructionNumber];     if (instruction.OpCode == OpCode.Ldstr &&         instructionNumber + 1 < method.Instructions.Length)     {         string loadString = instruction.Value.ToString(); 


Then we check that the next instruction is ResourceManager.GetString():

 Microsoft.Cci.Instruction nextInstruction =     method.Instructions[instructionNumber + 1]; if (nextInstruction.OpCode == OpCode.Callvirt &&     nextInstruction.Value is Microsoft.Cci.Method) {     Microsoft.Cci.Method nextInstructionMethod =         (Microsoft.Cci.Method) nextInstruction.Value;     if (nextInstructionMethod.FullName ==         "System.Resources.ResourceManager." +         "GetString(System.String)") 


Then we check that the associated fallback resource has the required string and, if not, report it as a problem:

 Resource resource; if (GetResource(method.DeclaringType, out resource, "Resources")) {     if (! KeyExistsInResource(resource, loadString))     {         Resolution resolution = GetResolution(             new string[] {method.Name.Name, loadString});         Problems.Add(new Problem(resolution));         return Problems;     } } 


We have seen the BaseGlobalizationRule.GetResource() method before, but in this example, we are passing a resource suffix, "Resources". This allows the Getresource method to search for resource names that are deliberately suffixed with "Resources". The KeyExistsInResource method is a simple scan of the resource:

 protected virtual bool KeyExistsInResource(     Resource resource, string keyName) {     ResourceReader reader = new ResourceReader(         new System.IO.MemoryStream(resource.Data));     try     {         IDictionaryEnumerator enumerator = reader.GetEnumerator();         while (enumerator.MoveNext())         {             if (enumerator.Key.ToString() == keyName)                 return true;         }     }     finally     {         reader.Close();     }     return false; } 





.NET Internationalization(c) The Developer's Guide to Building Global Windows and Web Applications
.NET Internationalization: The Developers Guide to Building Global Windows and Web Applications
ISBN: 0321341384
EAN: 2147483647
Year: 2006
Pages: 213

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