Checking Documentation Comments

[Note]Note

This section is optional. It discusses a useful, advanced application of custom FxCop rules to check XML documentation comments. If this is your first time reading this document, I strongly recommend skipping to the examples.

With some work, FxCop custom rules can be developed to check the XML documentation files generated by .NET compilers. For example, by checking assemblies against XML documentation files, you can verify that:

FxCop does not provide any built-in APIs to access the XML documentation file that is output by the compiler alongside the assembly. Fortunately, being XML, that file is fairly simple to parse using XmlDocument and other standard .NET XML APIs. The most significant hurdle lies in turning the names members (classes, methods, properties, and so on) into member "ID strings". Such strings are similar to the strings returned by the Member.FullName property. However, FxCop does not provide the right conversion function to the format used by documentation comments, and its implementation is relatively tricky. The syntax of ID strings is described by Microsoft under "Processing the XML File (C# Programming Guide)" at http://msdn2.microsoft.com/en-us/library/fsbx0t7x.aspx. Unfortunately, this official documentation does not completely describe the syntax required to support generic types, and it has some ambiguities and inconsistencies.

In this chapter, we present code that can generate a member ID string from a (hopefully arbitrary) Member node. I say hopefully due to the sheer complexity of this task. It is possible that some esoteric cases are not handled correctly by the code given here. Please report any such cases that you find.

This implementation differs from or expands on the Microsoft documentation mentioned above in at least the following ways:

  1. Parameters of ELEMENT_TYPE_PINNED types are not supported. I cannot figure out any way to generate such parameters in compilers. I think pinning applies only to local variables, not parameters. Thus, it looks like it is not applicable to XML documentation.

  2. Parameters of ELEMENT_TYPE_GENERICARRAY types are not supported. This term is absent from the ECMA CLI specification; I have no idea what the documentation is referring to.

  3. When a function pointer does have any parameters, the parameters are represented as (System.Void). Although the documentation states that the parentheses and parameters should be omitted in this case, this implementation is consistent with how the C++ compiler actually behaves.

  4. Template arguments are rendered within curly braces. This facet is not explained in the documentation.

  5. The names of methods with template parameters are suffixed with two backticks followed by the number of template parameters. The template parameters themselves are referenced with a double backtick notation. These facets are not explained in the documentation.

  6. The lower bounds of ELEMENT_TYPE_ARRAY arrays are always specified (often as 0). This seems consistent with the C# compiler.

The source code is given below. We will use this source code in a later example that demonstrates how to check that all externally visible members are documented.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

using Microsoft.FxCop.Sdk;

namespace TutorialRules
{
  sealed class DocComment
  {
    public static string GetMemberID(Member member)
    {
      char ch;
      TypeNode declaringType = member.DeclaringType;
      List<TypeNode> parentTypes = new List<TypeNode>();
      List<TypeNode> typeTemplateParameters = new List<TypeNode>();
      List<TypeNode> memberTemplateParameters = new List<TypeNode>();
      StringBuilder sb = new StringBuilder();

      if (member == null)
        throw new ArgumentNullException("member");

      // Determine prefix character.

      switch (member.NodeType)
      {
        case NodeType.Class:
        case NodeType.Interface:
        case NodeType.Struct:
        case NodeType.EnumNode:
        case NodeType.DelegateNode:
          ch = 'T';
          break;
        case NodeType.Field:
          ch = 'F';
          break;
        case NodeType.Property:
          ch = 'P';
          break;
        case NodeType.Method:
        case NodeType.InstanceInitializer:
        case NodeType.StaticInitializer:
          ch = 'M';
          break;
        case NodeType.Event:
          ch = 'E';
          break;
        default:
          throw new ArgumentException("Unsupported NodeType.", "member");
      }

      // Determine all parent types of this potentially nested type.

      for (TypeNode current = declaringType; current != null; current = current.DeclaringType)
      {
        parentTypes.Add(current);
      }
      parentTypes.Reverse();

      // Collect all template parameters for the types.

      foreach (TypeNode type in parentTypes)
      {
        if (type.TemplateParameters != null)
        {
          typeTemplateParameters.AddRange(type.TemplateParameters);
        }
      }

      // Collect all template parameters for the method.

      switch (member.NodeType)
      {
        case NodeType.Method:
        case NodeType.InstanceInitializer:
        case NodeType.StaticInitializer:
          Method method = (Method)member;

          if (method.TemplateParameters != null)
          {
            memberTemplateParameters.AddRange(method.TemplateParameters);
          }
          break;
      }

      // Output full method name.

      sb.Append(ch);
      sb.Append(':');
      if (declaringType == null)
      {
        TypeNode type = member as TypeNode;
        if (type != null)
        {
          if (type.Namespace.Name.Length != 0)
          {
            sb.Append(type.Namespace.Name);
            sb.Append('.');
          }
        }
      }
      else
      {
        sb.Append(declaringType.FullName.Replace('+', '.'));
        sb.Append('.');
      }
      sb.Append(member.Name.Name.Replace('.', '#'));

      // Output number of template parameters.

      if (memberTemplateParameters.Count != 0)
      {
        // Undocumented: based on output from MS compilers.
        sb.AppendFormat(CultureInfo.InvariantCulture, "``{0}", memberTemplateParameters.Count);
      }

      // Output parameters.

      ParameterCollection parameters;

      switch (member.NodeType)
      {
        case NodeType.Property:
          parameters = ((PropertyNode)member).Parameters;
          break;
        case NodeType.Method:
        case NodeType.InstanceInitializer:
        case NodeType.StaticInitializer:
          parameters = ((Method)member).Parameters;
          break;
        default:
          parameters = null;
          break;
      }

      if (parameters != null && parameters.Count != 0)
      {
        bool comma = false;
        sb.Append('(');
        foreach (Parameter parameter in parameters)
        {
          if (comma)
          {
            sb.Append(',');
          }
          sb.Append(GetStringForTypeNode(parameter.Type,
            typeTemplateParameters, memberTemplateParameters));
          comma = true;
        }
        sb.Append(')');
      }

      // Output return type (for conversion operators).

      if (member.NodeType == NodeType.Method && member.IsSpecialName &&
        (member.Name.Name == "op_Explicit" || member.Name.Name == "op_Implicit"))
      {
        Method convOperator = (Method)member;

        sb.Append('~');
        sb.Append(GetStringForTypeNode(convOperator.ReturnType,
          typeTemplateParameters, memberTemplateParameters));
      }

      return sb.ToString();
    }

    private static string GetStringForTypeNode(TypeNode type,
      List<TypeNode> typeTemplateParameters, List<TypeNode> memberTemplateParameters)
    {
      StringBuilder sb = new StringBuilder();

      switch (type.NodeType)
      {
        /* Ordinary types */

        case NodeType.Class:
        case NodeType.Interface:
        case NodeType.Struct:
        case NodeType.EnumNode:
        case NodeType.DelegateNode:
          if (type.DeclaringType == null)
          {
            if (type.Namespace.Name.Length != 0)
            {
              sb.Append(type.Namespace.Name);
              sb.Append('.');
            }
          }
          else
          {
            sb.Append(GetStringForTypeNode(type.DeclaringType,
              typeTemplateParameters, memberTemplateParameters));
            sb.Append('.');
          }

          if (type.IsGeneric)
          {
            String templateName = type.Template.Name.Name.Replace('+', '.');
            int pos = templateName.LastIndexOf('`');
            if (pos != -1)
            {
              sb.Append(templateName.Substring(0, pos));
            }
            else
            {
              sb.Append(templateName);
            }
          }
          else
          {
            sb.Append(type.Name.Name.Replace('+', '.'));
          }
          break;

        /* Simple pointer / reference types */

        case NodeType.Reference:
          sb.Append(GetStringForTypeNode(((Reference)type).ElementType,
            typeTemplateParameters, memberTemplateParameters));
          sb.Append('@');
          break;
        case NodeType.Pointer:
          sb.Append(GetStringForTypeNode(((Pointer)type).ElementType,
            typeTemplateParameters, memberTemplateParameters));
          sb.Append('*');
          break;

        /* Generic parameters */

        case NodeType.ClassParameter:
        case NodeType.TypeParameter:
          int index;
          if ((index = typeTemplateParameters.IndexOf(type)) != -1)
          {
            sb.AppendFormat(CultureInfo.InvariantCulture, "`{0}", index);
          }
          else if ((index = memberTemplateParameters.IndexOf(type)) != -1)
          {
            // Undocumented: based on output from MS compilers.
            sb.AppendFormat(CultureInfo.InvariantCulture, "``{0}", index);
          }
          else
          {
            throw new InvalidOperationException("Unable to resolve TypeParameter to a type argument.");
          }
          break;

        /* Arrays */

        case NodeType.ArrayType:
          ArrayType array = ((ArrayType)type);
          sb.Append(GetStringForTypeNode(array.ElementType,
            typeTemplateParameters, memberTemplateParameters));
          if (array.IsSzArray())
          {
            sb.Append("[]");
          }
          else
          {
            // This case handles true multidimensional arrays.
            // For example, in C#: string[,] myArray
            sb.Append('[');
            for (int i = 0; i < array.Rank; i++)
            {
              if (i != 0)
              {
                sb.Append(',');
              }

              // The following appears to be consistent with MS C# compiler output.
              sb.AppendFormat(CultureInfo.InvariantCulture, "{0}:", array.GetLowerBound(i));
              if (array.GetSize(i) != 0)
              {
                sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", array.GetSize(i));
              }
            }
            sb.Append(']');
          }
          break;

        /* Strange types (typically from C++/CLI) */

        case NodeType.FunctionPointer:
          FunctionPointer funcPointer = (FunctionPointer)type;
          sb.Append("=FUNC:");
          sb.Append(GetStringForTypeNode(funcPointer.ReturnType,
            typeTemplateParameters, memberTemplateParameters));
          if (funcPointer.ParameterTypes.Count != 0)
          {
            bool comma = false;
            sb.Append('(');
            foreach (TypeNode parameterType in funcPointer.ParameterTypes)
            {
              if (comma)
              {
                sb.Append(',');
              }
              sb.Append(GetStringForTypeNode(parameterType,
                typeTemplateParameters, memberTemplateParameters));
              comma = true;
            }
            sb.Append(')');
          }
          else
          {
            // Inconsistent with documentation: based on MS C++ compiler output.
            sb.Append("(System.Void)");
          }
          break;
        case NodeType.RequiredModifier:
          RequiredModifier reqModifier = (RequiredModifier)type;
          sb.Append(GetStringForTypeNode(reqModifier.ModifiedType,
            typeTemplateParameters, memberTemplateParameters));
          sb.Append("|");
          sb.Append(GetStringForTypeNode(reqModifier.Modifier,
            typeTemplateParameters, memberTemplateParameters));
          break;
        case NodeType.OptionalModifier:
          OptionalModifier optModifier = (OptionalModifier)type;
          sb.Append(GetStringForTypeNode(optModifier.ModifiedType,
            typeTemplateParameters, memberTemplateParameters));
          sb.Append("!");
          sb.Append(GetStringForTypeNode(optModifier.Modifier,
            typeTemplateParameters, memberTemplateParameters));
          break;

        default:
          throw new ArgumentException("Unsupported NodeType.", "type");
      }

      if (type.IsGeneric && type.TemplateArguments.Count != 0)
      {
        // Undocumented: based on output from MS compilers.
        sb.Append('{');
        bool comma = false;
        foreach (TypeNode templateArgumentType in type.TemplateArguments)
        {
          if (comma)
          {
            sb.Append(',');
          }
          sb.Append(GetStringForTypeNode(templateArgumentType,
            typeTemplateParameters, memberTemplateParameters));
          comma = true;
        }
        sb.Append('}');
      }

      return sb.ToString();
    }

    // Prevent instantiation of this class; all members are static.
    private DocComment()
    {
    }
  }
}