Share via


XML Comments

Document Your Code in No Time At All with Macros in Visual Studio

Tony Chow

Parts of this article are based on a prerelease version of Visual Studio 2005. Those sections are subject to change.

This article discusses:

  • Macros and the Visual Studio extensibility model
  • How to document a constructor automatically
  • Documenting exceptions thrown from methods and properties automatically
  • How to automatically copy XML documentation into a descendent class from its base type
This article uses the following technologies:
Visual Basic, C#, Visual Studio

Code download available at:XMLComments.exe(119 KB)

Contents

Documenting Constructors
Getting Started
Getting at the Constructor
Generating the XML Comment Block
Injecting the XML Comment Block
What About the Parameters?
Documenting Exceptions
Obtaining the Source Code
Newly Thrown Exceptions
Rethrown Exceptions
From Theory to Practice
Emulating Documentation "Inheritance"
Walking the Inheritance Tree
Conclusion

One of the most widely acclaimed features of the C# programming language is XML-based code annotation. Starting in Visual Studio® 2005, XML documentation support will also become available in Visual Basic® and C++. Yet in everyday practice, making full use of the many comment tags can be a tedious chore, and as a result, most managed code in the field remains woefully under-documented.

In this article, I will demonstrate how Visual Studio macros can be used to automate the creation of XML documentation. I'll create macros that write large sections of comments with just a keystroke. At the end of the day, nothing can completely automate code commenting, but a few well-placed macros will make your job substantially easier, eliminating much boilerplate work and allowing you to concentrate only on substantive comments that must be written by hand. And, you can create commenting macros for specific tasks based on the techniques I describe here.

Documenting Constructors

Let's begin with a relatively simple example that will introduce the essential features of Visual Studio extensibility. When writing XML documentation, the summary statements for constructors are among the most formulaic. Microsoft documentation typically observes the following format:

Initializes an instance of the <see cref="TypeName"/> class.

Any additional information is simply appended to the end of the sentence. The routine nature of constructor summaries makes them a prime candidate for automation, as well as the objective of the first macro I'll discuss here. When invoked, the macro will determine whether the current editing caret in the active code file is positioned within a class constructor. If it is, the macro will automatically create the summary statement and apply it to the constructor method.

With Visual Studio 2005 looming on the horizon, I'll make sure that this macro works on all versions of C# as well as in Visual Basic 2005. At the time of writing, Visual C++® 2005 does not support Visual Studio extensibility as well as the other languages do, so I won't be covering it here.

Getting Started

To start authoring macros, you first need to fire up the macros IDE. This is done from the main Visual Studio window with the menu item Tools | Macros | Macros IDE. The resulting window, shown in Figure 1, looks very much like Visual Studio itself, except that it shows a special macros project called MyMacros.

Figure 1 The Macros IDE

Figure 1** The Macros IDE **

All macros in Visual Studio .NET are written in Visual Basic .NET. Within the macros IDE, they are organized into Visual Basic modules. Each public parameterless Sub method in a public module is recognized as a macro by Visual Studio. In the "MyMacros" project you may find an existing module named RecordingModule. This is the module into which recorded macros are stored.

As a container for all the macros and helper routines I'll be writing, I'll create a new module and name it XmlCommentMacros. If you look inside the module file, you should see that the EnvDTE namespace is imported by default. This is the COM automation library for the Visual Studio extensibility model, and you'll need it. For the needs of the macros, you should import the following additional namespaces: System, System.Xml, System.IO, System.Collections, System.Text, and System.Text.RegularExpression. In order to support the System.Xml namespace, you will also need to add a reference to the System.Xml assembly to the macros project. You'll then create a public Sub method named DocumentConstructor. So far, the module should look like the following:

Imports EnvDTE Imports System Imports System.IO Imports System.Collections Imports System.Xml Imports System.Text Imports System.Text.RegularExpressions Public Module XmlCommentMacros Public Sub DocumentConstructor() End Sub End Module

At this point, if you return to the main Visual Studio window and open the Macros Explorer tool window, you should see the DocumentConstructor macro displayed nested within the XmlCommentMacros module node. You have created the skeleton of your constructor-documenting macro and can now proceed to fill it with logic.

Getting at the Constructor

When the macro is executed, the first order of business is to determine whether or not the editing caret in the active code file currently resides in a constructor. The position of the editing caret is discovered with the following line of code:

Dim caretPosition As TextPoint = DTE.ActiveDocument.Selection.ActivePoint

So far, things are pretty self-explanatory. The DTE global object is the entry point into the Visual Studio extensibility model. Within it, the DTE.ActiveDocument object represents the currently active code file in the Visual Studio IDE. Using the Selection property of the ActiveDocument object, you are then able to retrieve the position of the editing caret as a TextPoint object.

The TextPoint object is your ticket to the most powerful feature of Visual Studio extensibility, namely, CodeModel. CodeModel maintains a structured, language-agnostic view of all code within a project, organizing code elements such as classes, methods, properties, and namespaces into interlinked trees of CodeElement objects that are updated in real-time as you modify your code. All code elements implement the CodeElement interface, which provides generic information such as the name of the element. In addition, each object also implements at least one interface that exposes functionalities specific to its type. For example, code elements that represent classes implement the CodeClass interface, property elements implement the CodeProperty interface, and, in turn, namespace elements implement the CodeNameSpace interface. Figure 2 illustrates CodeModel's most important interfaces and its relationship to the other components of the extensibility model.

Figure 2 General Structure of CodeModel

With the TextPoint object, access to CodeModel is provided by the CodeElement property. This read-only property accepts a single enumerated parameter of type vsCMElement, identifying the type of code element that you are interested in retrieving. When invoked, the CodeElement property examines the code file from the current point upwards and returns the first CodeElement object, if any, that is of the specified type and which encapsulates the TextPoint. In this case, you need to obtain a reference to an object constructor, and since object constructors are identified as "functions" by CodeModel, you should attempt to retrieve a code element of type vsCMElementFunction:

Dim element As CodeElement = _ caretPosition.CodeElement(vsCMElement.vsCMElementFunction)

A call to TextPoint.CodeElement returns a null reference if no matching code element is found. If the "element" variable turns out to be a non-null value, you will know that the editing caret does indeed reside in a method declaration. What you don't know yet is whether this method is a constructor. This bit of information is easily obtainable by casting the variable into a CodeFunction interface, and examining its FunctionKind property. If the "element" variable is null, or if the object method it represents does not happen to be a constructor, the macro should inform the user and then gracefully exit:

Dim func As CodeFunction = element If func is Nothing OrElse func.FunctionKind <> _ vsCMFunction.vsCMFunctionConstructor Then MsgBox("Constructor not found.") Exit Sub End If

Generating the XML Comment Block

If the macro has not terminated itself by now, you have successfully acquired a CodeFunction object representing a constructor method, for which you are now poised to generate the XML documentation. Since there are no better tools at dealing with XML than those built into the Microsoft® .NET Framework, you'll be editing the XML comment block as a System.Xml.XmlDocument object. Ideally, the macro should also be able to detect the presence of an existing summary statement, and get confirmation from the user before overwriting its contents.

In this, you are greatly aided by the availability of the DocComment property on most code element interfaces, including CodeFunction. This string property sets or returns the XML comment block associated with the code element. Since later you'll need to extract existing comment tags from the code and place them in an XmlDocument, you encapsulate the logic in a private helper method named GetCommentXml, shown in Figure 3. From the main macro method, GetCommentXml is invoked and its return value placed in a variable named doc:

Dim doc As XmlDocument = GetCommentXml(func)

Figure 3 The GetCommentXml Helper

Private Function GetCommentXml(ByVal element As CodeElement) _ As XmlDocument Dim doc As XmlDocument = New XmlDocument() Dim xmlStr As String = element.DocComment If Not xmlStr Is Nothing AndAlso xmlStr.Trim() <> String.Empty Then If xmlStr.StartsWith("<doc>") = False Then ' Ensure single root node xmlStr = "<doc>" & xmlStr + "</doc>" End If xmlStr = Regex.Replace(xmlStr, "^(\s*<doc>\s*){2,}", "<doc>") xmlStr = Regex.Replace(xmlStr, "(\s*<\/doc>\s*){2,}$", "</doc>") doc.LoadXml(xmlStr) Else Dim rootTag = doc.CreateElement("doc") doc.AppendChild(rootTag) End If Return doc End Function

Let's examine what just occurred in GetCommentXml. In a nutshell, the method creates an XmlDocument, examines the DocComment property on the code element, and populates the former with the existing XML comment block. If existing comments are not found, a single root element named "doc" is created and appended to the XmlDocument object. No surprise here—a well-formed XML document must have a single root node.

You will notice, however, that some minor chicanery was required when dealing with the output of DocComment. This is necessitated by an unfortunate inconsistency between the way C# implements DocComment and the way Visual Basic does it in Visual Studio 2005, at least in Beta 2. Whereas the C# implementation returns the XML comment block already in a well-formed state (with a root <doc> node), Visual Basic simply gives you the naked comment block without the root node. This makes it compulsory in the latter case to manually wrap the return value of DocComment between <doc> tags, before you can pass it into the XmlDocument.LoadXml method. Also in Beta 2, in certain circumstances DocComment may mistakenly return a comment block with not one, but two <doc> tags, one nested inside the other. This necessitates a second workaround in GetCommentXml, where I use a regular expression to catch repeat occurrences of "<doc>" and "</doc>" and replace them with a single instance.

Having thus obtained the comment block as an XmlDocument object, you must now retrieve the <summary> tag from within the root node or, if one does not exist, create a new one. Should the <summary> tag already exist and have contents, the user must be warned about the impending overwrite before proceeding. You therefore continue the macro, using an XPath query to determine the existence of a <summary> tag:

Dim summaryTag As XmlElement = doc.SelectSingleNode("/doc/summary") If summaryTag Is Nothing Then ' Create <summary> tag summaryTag = doc.CreateElement("summary") doc.FirstChild.PrependChild(summaryTag) ElseIf summaryTag.InnerXml <> String.Empty Then ' Overwrite confirmation If MsgBox("Do you wish to overwrite the contents of the summary tag?", _ MsgBoxStyle.YesNo) = MsgBoxResult.No Then Exit Sub End If End If

With the XmlElement object representing the <summary> tag in hand, you can now create the summary statement for the constructor. Follow the established Microsoft format, with a cross reference to the class or structure served by the constructor:

<para>Initializes an instance of the <see cref="TypeName"/> class/structure.</para>

Determining the name of the parent type in CodeModel is as simple as accessing the Parent property of the CodeFunction object. As a nice touch, the summary should distinguish between a "class" and a "structure" when referring to the parent type. The following code wraps up the summary generation process:

Dim typeName As String = func.Parent.Name Dim typeCategory As String = _ IIf(func.Parent.Kind = vsCMElement.vsCMElementStruct, _ "structure", "class") Dim summary As String = String.Format( _ "<para>Initializes an instance of the <see cref=""{0}""/> {1}. </para>", typeName, typeCategory) summaryTag.InnerXml = summary

Injecting the XML Comment Block

Now that you've created the summary, all that's left to do is to inject the XML comment block back into the DocComment property. Naturally you'll need the output to be properly indented, and the easiest way to make it so is to employ the System.Xml.XmlTextWriter class. Like GetCommentXml, this is an operation that will be needed by all of your macros. Create a new helper method named SetCommentXml, as shown in Figure 4. This helper takes two arguments: a CodeElement and an XmlDocument. It first writes the contents of the XmlDocument into a properly indented string, and then assigns it to the code element's DocComment property. As in GetCommentXml, SetCommentXml needs to accommodate the different behaviors of C# 2.0 and Visual Basic 2005, in that the former demands a <doc> root node when setting the DocComment property, and the latter does not. This forces the code to vary the node at which XmlTextWriter starts writing, depending on the current language, which is determined by examining the CodeElement.Language property. Within the main method, an invocation to SetCommentXml completes the macro:

SetCommentXml(func, doc)

Figure 4 SetCommentXml Helper

Private Function SetCommentXml(ByVal element As CodeElement, _ ByVal doc As XmlDocument) Dim sb As StringBuilder = New StringBuilder() Dim sw As StringWriter = New StringWriter(sb) Dim xw As XmlTextWriter = New XmlTextWriter(sw) Try xw.Indentation = 1 xw.IndentChar = Char.Parse(vbTab) xw.Formatting = Formatting.Indented Dim rootNode As XmlNode If element.Language = CodeModelLanguageConstants.vsCMLanguageVB Then rootNode = doc.FirstChild Else ' CSharp rootNode = doc End If rootNode.WriteContentTo(xw) element.DocComment = sb.ToString() Finally xw.Close() sw.Close() End Try End Function

From the Visual Studio IDE, running the DocumentConstructor macro with the editing caret positioned within a constructor method written in C# or Visual Basic 2005 produces the respective outcomes shown in Figure 5. For even greater convenience, you can map the macro to a keystroke in the Visual Studio Options dialog. Documenting a constructor is now as convenient as hitting a key.

Figure 5 Running DocumentConstructor Macro

C#

public class Foo { /// <summary> /// <para>Initializes an instance of the <see cref="Foo"/> class. /// </summary> public Foo() { } }

Visual Basic 2005

Public Class Foo ''' <summary> ''' <para>Initializes an instance of the <see cref="Foo"/> class. ''' </summary> Public Sub New() End Sub End Class

What About the Parameters?

Alert readers might protest at this point, and rightly so, that the macro doesn't take into account parameterized constructors. Indeed, in both C# and Visual Basic 2005, as soon as you type the three slashes or apostrophes over a code element, IntelliSense® creates an XML comment skeleton for you, complete with tags for all parameters. Your macro here, in contrast, is blissfully unaware of any parameters. In this regard, it's less functional than the IDE's default behaviors. Fortunately, these functionalities are easily duplicated by a simple helper method. In addition to framing the <param> tags, this helper will also add other essential tags depending on the type of code element you pass to it, making it capable of generating comment skeletons on any code element, and not just constructors. Let's call it EnsureBasicTags. Figure 6 provides the complete code for the helper.

Figure 6 EnsureBasicTags Helper

Private Sub EnsureBasicTags(ByVal element As CodeElement, _ ByVal doc As XmlDocument) ' <summary> If doc.SelectSingleNode("/doc/summary") Is Nothing Then Dim summaryTag As XmlElement = doc.CreateElement("summary") summaryTag.AppendChild(doc.CreateTextNode("")) doc.FirstChild.PrependChild(summaryTag) End If ' <param> If element.Kind = vsCMElement.vsCMElementFunction Then For Each param As CodeParameter in element.Parameters Dim xpath As String = _ String.Format("/doc/param[@name='{0}']", param.Name) If doc.SelectSingleNode(xpath) Is Nothing Then Dim paramTag As XmlElement = doc.CreateElement("param") paramTag.SetAttribute("name", param.Name) paramTag.AppendChild(doc.CreateTextNode("")) doc.FirstChild.AppendChild(paramTag) End If Next End If ' <returns> If element.Kind = vsCMElement.vsCMElementFunction _ AndAlso element.Type.TypeKind <> vsCMTypeRef.vsCMTypeRefVoid _ AndAlso doc.SelectSingleNode("/doc/returns") Is Nothing Then Dim returnsTag As XmlElement = doc.CreateElement("returns") returnsTag.AppendChild(doc.CreateTextNode("")) doc.FirstChild.AppendChild(returnsTag) End If ' <value> If element.Kind = vsCMElement.vsCMElementProperty _ AndAlso doc.SelectSingleNode("/doc/value") Is Nothing Then Dim valueTag As XmlElement = doc.CreateElement("value") valueTag.AppendChild(doc.CreateTextNode("")) doc.FirstChild.AppendChild(valueTag) End If End Sub

The EnsureBasicTags helper method accepts two parameters: the CodeElement to document, and the XmlDocument containing the XML comment block being worked on. It creates the <summary> tag in all cases, <param> tags if the code element is a method, the <returns> tag for methods with a return value, and the <value> tag for properties. When creating the <param> tags, the method parameters are discovered by enumerating the Parameters collection of the CodeFunction interface. Using XPath queries, you'll also ensure that each tag is only added if it's not already present. All along the way, you have to append an empty TextNode object to every tag that is created in order to prevent XmlTextWriter from omitting the closing tag when rendering an XML element that has no contents.

Modify the DocumentConstructor method to invoke EnsureBasicTags right above the SetCommentXml call, and the macro is truly complete, as shown here:

EnsureBasicTags(func, doc)

In addition to being functional, this macro demonstrates many core concepts in Visual Studio extensibility. Armed with these insights, you can now venture into deeper waters.

Documenting Exceptions

Thorough documentation of all thrown exceptions can greatly enhance the usability of any class library. It lets users anticipate anomalous conditions and apply appropriate mitigative measures, without going through the pain of trial and error. In the .NET Framework, exception documentation is also the standard means of conveying acceptable values for method arguments. To encourage documenting exceptions, XML documentation formalizes the concept in the form of the <exception> tag. In spite of this, the <exception> tag remains one of the most underused in real-world coding. Part of the reason is the iron discipline required; it's a lot of work typing up a tag for each and every exception and describing the conditions that may cause it to be thrown.

Things are about to be made easier, however, by the second macro. When completed, this macro will examine a method or property and produce an <exception> skeleton for each type of exception explicitly thrown in the code. For the most commonly used exceptions, it will even write entire lines of comments, saving your time and sanity. Let's see how this is accomplished.

Like the first macro, the new macro will begin by obtaining the CodeElement object over which the editing caret is positioned. The specific requirements are a bit different, however. In the first macro, the only code elements of interest were methods. But since you are documenting exceptions, and exceptions can be thrown in methods and properties alike, you will now be interested in obtaining a CodeElement that represents either a method or a property. This service is best provided by another helper method. This helper must accept a parameter array of vsCMElement values, and call TextPoint.CodeElement repeatedly in a loop, looking for a code element that matches any of the specified types. Let's call the helper GetCurrentCodeElement, and define it as shown in Figure 7. As before, the helper has to work around the bug in the Visual Studio 2005 Beta by wrapping each call to TextPoint.CodeElement in a try/catch construct.

Figure 7 GetCurrentCodeElement Helper

Private Function GetCurrentCodeElement( _ ByVal ParamArray kinds() As vsCMElement) As CodeElement Dim caretPosition = DTE.ActiveDocument.Selection.ActivePoint For Each kind As vsCMElement in kinds Dim element As CodeElement = caretPosition.CodeElement(kind) If Not element Is Nothing Then Return element End If Next Return Nothing ' No matching element found. End Function

With this hurdle out of the way, you can begin writing the macro. In the XmlCommentMacros module, declare a new macro method named DocumentExceptions. The first line of this macro will invoke the GetCurrentCodeElement helper. If the helper does not find a method or property at the editing caret, the macro shall inform the user and exit. Otherwise, you will retrieve the XmlDocument for the CodeElement object by calling the GetCommentXml helper that was created earlier, followed by a call to EnsureBasicTags, so that in addition to documenting exceptions, you also create a skeleton of basic XML comment tags. The opening lines of the DocumentExceptions macro are shown in the code in Figure 8.

Figure 8 DocumentExceptions Excerpt

Public Sub DocumentExceptions() Dim element As CodeElement = GetCurrentCodeElement( _ vsCMElement.vsCMElementFunction, _ vsCMElement.vsCMElementProperty) If element Is Nothing Then MsgBox("This macro is only applicable to methods and properties.") Exit Sub End If Dim doc As XmlDocument = GetCommentXml(element) EnsureBasicTags(element, doc) End Sub

Obtaining the Source Code

In the first macro, you relied solely on the language-agnostic CodeModel objects to provide the necessary information about a constructor and the class it serves. But CodeModel only goes as deep as member declarations; it doesn't analyze what goes on within procedural code. This means that in your effort to discover thrown exceptions, you have to somehow obtain the actual source code and parse the code in a language-specific fashion.

This isn't as daunting a task as it may sound. With CodeModel, it's quite easy to access the source code specific to a code element. On each CodeElement object there are two properties, StartPoint and EndPoint, denoting the points in the code file where the code element starts and ends, respectively. Calling the CreateEditPoint method on the StartPoint object yields an EditPoint object on which the GetText method can then be invoked (with the EndPoint object as a parameter) to obtain the complete text of the source code. The following code appended to the macro does just that and stores the output in a variable:

Dim editPoint As EditPoint = element.StartPoint.CreateEditPoint() Dim sourceCode As String = editPoint.GetText(element.EndPoint)

When executed on C#, the sourceCode variable will contain the source code from the declaration line of the code element, all the way to its closing bracket. For Visual Basic 2005, it holds the code from the member declaration to the "End Function/Sub/Property" statement of the same member.

Source code in hand, you are now faced with the more challenging part of the process: parsing the code. This is something that a few well-thought-out regular expressions can easily take care of, however. Let's consider the various ways exceptions can be thrown in code, and design a regular expression to match each scenario. Note that you're only looking for exceptions explicitly thrown in the code body itself; you won't look for exceptions that might be thrown from invocations within the method or property.

Newly Thrown Exceptions

When an exception is thrown in your code, it is usually done by instantiating an exception object and passing it to the "throw" keyword, all in a single statement. The syntax is very similar in C# and Visual Basic. Of the newly thrown exceptions, the most commonly used are those derived from the ArgumentException base class, including ArgumentNullException, ArgumentOutOfRangeException, and ArgumentException itself. These also happen to be the exceptions for which the macro can generate entire comments.

With these exception classes, the constructor invocation usually accepts an argument that specifies the name of the faulty parameter that has caused the exception to be thrown. The name of the exception, together with the affected parameter name, are all that you need to formulate reasonably informative comments. Take, for instance, the following C# method, which accepts four param-eters and which throws an ArgumentNullException for two of its parameters, an ArgumentOutOfRangeException for the third parameter, and an ArgumentException for the fourth:

public void Bar(string arg1, string arg2, int index, object arg4) { if (arg1 == null) throw new ArgumentNullException("arg1"); if (arg2 == null) throw new ArgumentNullException("arg2"); if (index < 0) throw new ArgumentOutOfRangeException("index"); if (arg4 == this) throw new ArgumentException("Invalid.", "arg4"); }

If the macro could identify the exceptions thrown in this code and extract the name of each exception class along with the name of the affected parameter in each case, it wouldn't take much effort to generate the XML documentation shown in Figure 9.

Figure 9 DocumentExceptions Sample Output

<exception cref="ArgumentNullException"> <para>The argument <paramref name="arg1"/> is <langword name="null"/>.</para> <para>-or-</para> <para>The argument <paramref name="arg2"/> is <langword name="null"/>.</para> </exception> <exception cref="ArgumentOutOfRangeException"> <para>The argument <paramref name="index"/> is out of range.</para> </exception> <exception cref="ArgumentException"> <para>The argument <paramref name="arg4"/> is invalid.</para> </exception>

Notice that, in keeping with the Microsoft practice, only one <exception> tag is created for each type of exception thrown. If an exception appears more than once, an "-or-" paragraph is used to delimit the various conditions under which it can be incurred. Admittedly, the comment that was generated for ArgumentException is less informative than the other comments, but it would still save a lot of typing by providing a template to which more specific information can be added.

Instances of the ArgumentNullException and ArgumentOutOfRangeException, along with the values of their paramName arguments, can be identified and captured with a single regular expression that works with code written in either C# or Visual Basic, as you see in the following:

(?i:\bthrow\s+new\s+(?<exception>Argument(Null|OutOfRange)Exception)\s*\ (\s*"(?<param>\w+)")

This expression extracts the exception information that follows each "throw new" phrase, while ignoring whitespace. The "exception" group captures the exact name of the exception, while the "param" group captures the name of the affected parameter. Visual Basic support is achieved with a (?i:) grouping construct that makes the whole expression case-insensitive.

Capturing instances of ArgumentException demands a separate regular expression, for, unlike the other two exception classes, the constructor of ArgumentException accepts the name of the affected parameter as its second, rather than first argument. Once again, this regular expression applies to either C# or Visual Basic:

(?i:\bthrow\s+new\s+(?<exception>ArgumentException)\s*\ (\s*".*?",\s*"(?<param>\w+)")

Here, the first constructor argument is ignored, leaving the second argument to be captured as the name of the affected parameter. It is worth pointing out that the liberal use of "\s*" and "\s+" patterns in both of these expressions makes them highly tolerant of extra spaces that may appear in the source code by accident or design. This is a good practice that you should always follow when creating regular expressions.

For other types of exceptions, it's more difficult to derive sufficient information from the source code to generate complete comments. Nonetheless, it would still be tremendously labor-saving to emit <exception> skeletons in the XML comment block, one for each exception type. To facilitate this, I use another regular expression that captures only the name of the exception class:

(?i:\bthrow\s+new\s+(?<exception>[\w\.]*Exception\b))

As before, by virtue of being case-insensitive, this regular expression works with C# and Visual Basic alike. For the time being, you need not worry about overlapping the exceptions already captured by the other two regular expressions.

Rethrown Exceptions

Aside from newly instantiated exceptions, the macro also needs to handle exceptions that are captured by a try/catch clause in either C# or Visual Basic and then rethrown. In this usage, C# and Visual Basic are sufficiently different to warrant a separate regular expression for each. For C#, the expression to recognize and capture a rethrown exception can be written as follows:

(?s:(?<indent>\n\s*)catch\s*\(\s*(?<exception>[\w\.]+)\b.*?\).*?\bthrow\s* (\s+\w+)?;.*?\k<indent>\})

This regular expression is more complex than the ones you've seen thus far and merits some explanation.

Since the pattern spans multiple lines, you need to switch on the single-line mode by encapsulating the entire expression in a (?s:) grouping construct, so that the new line character "\n" is matched by the "." and "\s" character classes. In essence, the responsibility of this expression is to match any block of text that begins with a "catch" keyword, contains a "throw" keyword, and ends with the first occurrence of a closing bracket that has the same level of indentation as the aforementioned "catch". The matching of indentation levels is accomplished by first capturing the indenting spaces ahead of the "catch" keyword in the "indent" group, and then back-referencing the captured characters with a "\k" clause ahead of a closing bracket. Along the way, the name of the exception is captured by the "exception" group.

The Visual Basic version of this regular expression follows much of the same principles, with two distinct characteristics of the language necessitating modifications. Instead of a closing bracket, you have to match an "End Catch" phrase. Also, in the Visual Basic version of the try/catch clause, the variable to store the exception object is not optional. Of course, Visual Basic is also case-insensitive. Taking these various factors into account, you end up with the following expression:

(?si:(?<indent>\n\s*)Catch\s+\w+\s+As\s+(?<exception>[\w\.]+).*?\bthrow\s* (\s+\w+)?(?=\s*\n.*?)\k<indent>End\s+Try)

From Theory to Practice

These, then, are all the regular expressions that you need to perform basic parsing of the source code. The remaining code of the DocumentExceptions macro is available in the code download. You first gather all the regular expressions needed for the current language into an ArrayList collection. Then you enumerate this collection, running each expression over the source code of the method or property being documented. For each type of exception found, your macro uses an XPath query to ascertain whether an <exception> tag already exists for it. If none exists, a new tag is created, ensuring that at the very least an XML skeleton is generated for each exception thrown. The XPath query also makes sure that each exception type is never documented twice, even if it's caught by more than one regular expression.

If a "param" group is found within a regular expression match, there's more work to be done because it signals that you've encountered an argument exception for which a comment can be automatically generated. To prevent the same parameter name from being documented more than once for each exception type, the code should issue another XPath query to determine whether a reference to the parameter, in the form of a <paramref> tag, already exists within the <exception> tag. If it does not, proceed to construct the comment pertaining to the parameter. The finished comment is appended to the <exception> tag's InnerXml property, preceded by an "-or-" paragraph if needed, in order to delimit it from other comments.

After all of the regular expressions have been executed over the source code, the macro should end by calling the SetCommentXml helper method to emit the finished XML comment block back into the code file.

Run the DocumentExceptions macro over a method or property written in either C# or Visual Basic 2005, and the resulting XML documentation should account for most exceptions explicitly thrown in the code. For programmers already steeped in sound exception practices and who aspire to develop the documentation skills to match, this macro is certain to save a tremendous amount of time and labor. Comprehensive exception documentation has suddenly become a more viable proposition.

Emulating Documentation "Inheritance"

For the third and final exercise, you'll muster everything you've learned so far to tackle one of the greatest stumbling blocks in achieving comprehensive documentation coverage of your code—the absence of a "documentation inheritance" feature in Visual Studio. If you've dealt with XML documentation for any length, you are more than likely irritated by the fact that when overriding a method or property in a child class, or implementing an interface member, the XML documentation from the base type is not carried over. This is frustrating, because it forces you to either document the member from scratch, or hand-copy comments from the base type. While you may not necessarily want an overridden member to be documented with exactly the same text as in the base type, it's at least handy to have the base XML documentation as a template to work from.

Here as before, the rich capabilities of CodeModel make it almost trivial to provide this feature, the goal of the third macro. When this macro is invoked over a method or property, it will walk the inheritance hierarchy, find the original XML comments for that member from the base type, and copy it into your code.

What enables you to do all this is the fact that CodeModel keeps track of the inheritance relationships of all types declared or referenced in the project. This information is exposed through two properties available on both the CodeClass interface and the CodeStruct interface: the Bases property and ImplementedInterfaces property. The Bases property is a collection of all direct parent classes and never has more than one item in it, since .NET only permits single implementation inheritance. The ImplementedInterfaces property is a collection of all interfaces directly implemented by the current type. By enumerating these two collections in a recursive fashion, you can discover all of the base types associated with the current class or structure, identify the base implementation of the property or method being documented, and extract the documentation from it.

With this in mind, let's begin writing the macro, which you'll call InheritDocumentation. The macro starts much as the others did, with an invocation to the GetCurrentCodeElement helper to obtain either a method or property code element at the editing caret. As before, if a method or property cannot be found, the user is warned and the macro exits. At this point, the macro should look like the following:

Public Sub InheritDocumentation() Dim element As CodeElement = GetCurrentCodeElement( _ vsCMElement.vsCMElementFunction, vsCMElement.vsCMElementProperty) If element Is Nothing Then MsgBox("This macro is only applicable to methods, and properties.") Exit Sub End If End Sub

Even if a method or property is successfully obtained, some additional filtering can be performed to ensure that you don't waste time on an ineligible code element. A private member, for instance, cannot possibly derive from a base type. Likewise, a static member is not applicable for documentation inheritance either. The accessibility of the member can be determined by examining the Access property, which is exposed by both the CodeFunction and CodeProperty interfaces.

Determining whether a member is static is a bit more difficult. To be sure, there's an IsShared property on the CodeFunction interface, but the same cannot be said of the CodeProperty interface. This seeming inconsistency exists because the code model reflects the fact that a property is actually an amalgamation of two accessor methods. In turn, the accessor methods are available as CodeFunction objects through the CodeProperty.Getter and CodeProperty.Setter properties, and it is by examining the value of the IsShared property on either accessor that you can tell whether a property is static. Instead of writing a contorted "If" statement, it is better to create a simple helper function that takes either a method element or a property element as the argument and returns a simple Boolean value indicating whether the member is static. The code for the IsMemberStatic helper is as follows:

Private Function IsMemberStatic(ByVal member As CodeElement) As Boolean If member.Kind = vsCMElement.vsCMElementFunction Then Return member.IsShared ElseIf Not member.Getter Is Nothing Then Return member.Getter.IsShared ElseIf Not member.Setter Is Nothing Then Return member.Setter.IsShared Else Return False EndIf End Function

If, with the help of IsMemberStatic, the macro finds that the current member is either private or static, it should inform the user and then gracefully exit, like so:

If element.Access = vsCMAccess.vsCMAccessPrivate _ OrElse IsMemberStatic(element) Then MsgBox("Not applicable to static or private members.") Exit Sub End If

Of course, the macro will also need to warn the user if any comments exist on the member since they will be overwritten with the base documentation. The easiest way to tell whether there are existing comments is to make a call to the GetCommentXml helper method and see if the root node of the returned XmlDocument contains any child nodes:

If GetCommentXml(element).FirstChild.ChildNodes.Count > 0 Then If MsgBox("Overwrite the current documentation " _ + "with inherited documentation?", MsgBoxStyle.YesNo) = MsgBoxResult.No Then Exit Sub End If End If

Walking the Inheritance Tree

After taking all possible precautions and obtaining the CodeElement object representing the current method or property, it is time to walk up the inheritance tree of the class or structure and look for the base member that it implements or overrides. This service is best provided by a helper method which I'll name FindBaseMember, shown in its entirety in Figure 10. The FindBaseMember helper is meant to be invoked recursively and takes two arguments: a CodeElement representing the method or property being documented, and another CodeElement representing the class, structure, or interface that is currently being examined.

Figure 10 The FindBaseMember Helper Method

Private Function FindBaseMember(ByVal descendentMember As CodeElement, _ ByVal typeElement As CodeElement) As CodeElement If descendentMember.Parent.FullName <> typeElement.FullName Then For Each member As CodeElement In typeElement.Members If IsMemberStatic(member) = False _ AndAlso member.Name = descendentMember.Name _ AndAlso member.Kind = descendentMember.Kind _ AndAlso member.Type.AsString = _ descendentMember.Type.AsString Then If member.Kind = vsCMElement.vsCMElementFunction Then ' Compare method parameters If member.Parameters.Count <> _ descendentMember.Parameters.Count Then GoTo MemberEnd End If For i As Integer = 1 To member.Parameters.Count Dim paramX As CodeParameter = _ member.Parameters.Item(i) Dim paramY As CodeParameter = _ descendentMember.Parameters.Item(i) If paramX.Type.AsString <> paramY.Type.AsString Then ' Different overloads: skip GoTo MemberEnd End If Next End If ' Matching base member found Return member End If MemberEnd: Next End If ' Matching base member not found, keep looking Dim baseTypeList As ArrayList = New ArrayList() For Each baseType As CodeElement In typeElement.Bases baseTypeList.Add(baseType) Next If Not TypeOf (typeElement) Is CodeInterface Then For Each baseInterface As CodeElement In _ typeElement.ImplementedInterfaces baseTypeList.Add(baseInterface) Next End If For Each baseType As CodeElement In baseTypeList Dim baseMember As CodeElement = _ FindBaseMember(descendentMember, baseType) If Not baseMember Is Nothing Then Return baseMember End If Next ' No base member found Return Nothing End Function

The first invocation of FindBaseMember is made within the macro method, with the class or structure of the documented member as the second argument:

Dim baseMember As CodeElement = FindBaseMember(element, element.Parent)

This sets the ball rolling. Each time FindBaseMember is invoked, it enumerates the members of the specified type and looks for one with the same name, accessibility, and signature as the method or property being documented. If no matching member is found on the specified type, or if the specified type is the same as the one on which the documented member occurs, FindBaseMember climbs further up the inheritance tree. It enumerates all direct base types gathered from the Bases and ImplementedInterfaces collections and invokes itself recursively with each base type as the argument. The final output of each FindBaseMember recursion is either a CodeElement representing the first matching base member found in the inheritance tree, or a null reference if no matching base member can be found.

Once the return value of FindBaseMember is in the bag, the rest of the macro is straightforward, as shown in Figure 11.

Figure 11 Using Results of FindBaseMember

If baseMember Is Nothing Then MsgBox("Base member not found. No documentation can be copied.") Exit Sub End If Dim doc As XmlDocument = GetCommentXml(baseMember) If doc.FirstChild.ChildNodes.Count = 0 Then MsgBox("Base documentation not found.") Exit Sub End If SetCommentXml(element, doc)

The macro informs the user and exits if FindBaseMember has returned a null reference. Otherwise, the GetCommentXml helper is invoked on the base member to retrieve its XML documentation. If the base member itself turns out to be undocumented, the macro informs the user and exits. Finally, SetCommentXml is invoked, effectively copying the documentation from the base class to the active code file.

The InheritDocumentation macro is now complete. Simply execute it over a method or property that overrides or implements an XML-documented base member, and the base documentation is instantly cloned. While you may still want to tweak the text to your liking, the amount of work saved by this macro is significant.

Conclusion

A new version of Visual Studio is upon us, and with great new IDE features like Code Snippets getting all the press, it's easy to forget that macros have always offered awesome automation capabilities, many of which no canned features can ever hope to match. When applied to the task of creating XML documentation, macros are especially rewarding, resulting in much better code documentation with far less effort. In the preceding discussion, I've only scratched the surface of what you can accomplish using macros. The rest is for you to discover.

Tony Chow is a developer and consultant specializing in the .NET Framework. He is based in Los Angeles, California. You can reach Tony at tony@bluetentacle.com.