Filed under Development
People who has been working with me knows that I'm a big fan of FxCop (or Code Analysis), I think static code analysis tools are very useful and a must-have in any development team, since ensure developers follow the company's guidelines and write correct code (at least it helps a lot). In addition, I see them also as a great ally to improve the learning curve of new developers.
While FxCop analyzes managed code assemblies checking for improvements about design, localization, performance, security, interoperability, etc. StyleCop focuses mainly in code style guidelines.
In the previous post Xml Documentation Comments - Exceptions I we saw how to document exceptions using the Xml Documentation features. Here we will see how to take profit of StyleCop SDK to create a custom rule that validates we are documenting those exceptions.
To create our rule we just need to create a class library project, add references to the assemblies Microsoft.StyleCop.dll and Microsoft.StyleCop.CSharp.dll, create a class that inherits from SourceAnalyzer, add an attribute and embed an Xml file with the necessary Metatada for our rules. All these steps are entirely documented in the StyleCop SDK documentation, so I do not waste too much time explaining it.
Before we start we need to know that the StyleCop engine incorporates a custom C# language service that is used to create a rich model representing the original C# code. What our rule will do is to use that model to visit the different elements that can throw exceptions, analyze the element's body to find the throw statements and extract the type of that exceptions, it will extract the exceptions documented using the Xml comments, finally it will compare the thrown exceptions with the documented ones and will add a violation for all the discrepancies found.
We know that we can use throw statements inside methods (including constructors), indexers and properties. So, taking profit of the Visitor implementation that the API provide us we will visit all the CsElement of a CsDocument looking for the declared methods, indexers and properties. Below you can see the callback method we have passed to the WalkDocument method.
1: private bool VisitElements(
2: CsElement element,
3: CsElement parentElement,
4: List<string> usingDirectives)
6: if (this.Cancel)
8: return false;
11: // We store the using directives for future use
12: if (element.ElementType == ElementType.UsingDirective)
18: // There are severeal element types that can throw exceptions:
19: // Methods, Constructors, Indexers and Properties
20: if (!element.Generated &&
21: (element.ElementType == ElementType.Method ||
22: element.ElementType == ElementType.Constructor ||
23: element.ElementType == ElementType.Indexer ||
24: element.ElementType == ElementType.Property))
30: return true;
Lines 17 to 24 allow us filtering the elements for which we are going to check the documentation. You can see also from line 10 to 13 that we store the using directives defined in the document, later on I will explain why.
The next is to analyze more in detail each element we have filtered, what we will do is first look for the exception tags added in the Xml comments and then compare them with the type of the exceptions thrown in the given element. The first step is as easy as perform a Linq Xml query over a property called Header the CsElement has. For the second one we will create a CodeWalkerStatementVisitor that we will pass to the WalkElement method, this callback will be used to iterate the element's statements looking for all the the throw statements defined.
To do it we only need to check the StatementType property belogning to the class Statement and make sure its type is StatementType.Throw. Once we have the ThrowStatement we can investigate the type of the exception that will be thrown. The demo rule can analyze the next scenarios:
- New expressions. i.e. throw new System.Exception();
- Literal expressions. i.e. var ex = new Exception(); throw ex;
- Method invocation expressions. i.e. throw GetMyExceptionInstance();
The ThrowStatement class has a property called ThrownExpression that allow us identifying the expression used to throw the exception.
For the first scenario, we need to iterate the different tokens of the NewExpression and extract the exception type. This is done in the code using the next method.
1: private static string ExtractExceptionType(NewExpression expression)
3: for (Node<CsToken> node = expression.Tokens.First; node != null;
4: node = node.Next)
6: CsToken token = node.Value;
7: if (token.CsTokenType == CsTokenType.Other)
9: return token.Text;
13: return string.Empty;
For the second scenario we have a small handicap, the literal represents a variable that is not defined in the throw statement, instead it is defined in upper statements or even in elements different than the one where the exception is thrown i.e.
1: ArgumentNullException ex = new ArgumentNullException();
2: public void DoSomething(string value)
4: if (value == null)
6: throw new ex;
In the code above the element containing the throw statement is the method DoSomething, but the variable ex is defined in a class element.
For this reason before we can obtain the type of the exception, we first need to retrieve the variables defined by the parent statements of the throw statement. After that we just need to find the variable among the retrieved ones to determine its type and return it for further analysis.
The third scenario is the MethodInvocationExpression, here we have the biggest handicaps because there are many possibilities in which we can obtain an exception from a method. The easiest case is a direct method call, we will have MethodInvocationExpression containing the LiteralExpression with the name of the method, but imagine the next situation:
throw new MyClass().GetException();
In this case, the expression is a member access and we need to go to the right side to find the literal with the name of the method.
Once we have the obtained the name of the method, we can perform a search in the Document looking for that Method element in order to retrieve its return type that, of course, will be our exception type. We do not need to worry about overloads with different return types, because even if the CLR supports it C# does not and we are analyzing C# code.
At this point we have been able to retrieve all the exception types thrown by the throw statements and we can compare with the ones we have documented. Unfortunately we are not done yet and there is another point to solve.
The problem we have is that the object model representing the code we analyze, does not stores the type of the variables , return types of methods, etc. as the .NET type "Type". Instead they are represented in best case as a TokenType with a text representation. This has sense for many reasons, one is because StyleCop analyzes code that doesn't necessarily compiles therefore it's possible the type does not exists yet.
So, how this affects to the rule? Since we do not have the real types for the Exceptions we want to verify but string representations of them, the next code will cause a false positive:
1: /// <exception cref="System.Exception"></exception>
2: public void DoSomething()
4: throw new Exception();
The false positive appears because at the time we compare the type throw with the type documented we will compare "System.Exception" with "Exception". That's the reason why we have stored the using directives in a previous step, because before to compare we will try to obtain the full type name doing a Type.GetType() concatenating the different namespaces imported into the document. In case, that we are still not able to get the type (i.e. exception defined in a different assembly not accessible from StyleCop) we will compare both texts without the namespace, this is using Exception instead of System.Exception.
I'm really curious to see if StyleCop will be benefited when the new managed C# compiler is there, part of the project to offer the C# compiler as a service will include a complete language object model. I suppose it will be a natural step to use that new model, but this is way far from today since, regardless the C# team is already working on it, it is supposed to come after C# 4.0.
With this rule we are covering the most common scenarios where exceptions are thrown, but there are cases that will fail. i.e. If the exception thrown is obtained from a method declared in a different document, the rule will not be able to find that method and consequently the type of the exception.
StyleCopRule.zip (21.66 kb)