Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect compiler generated default constructor using reflection in C#

I'm targeting .NET 3.5 SP1 and I'm using CommentChecker to validate my XML documentation, everything works OK until I get to a class like this:

/// <summary>
/// documentation
/// </summary>
public sealed class MyClass {
    /// <summary>
    /// documentation
    /// </summary>
    public void Method() {
    }
}

In the example above, as I understand, the compiler generates a default constructor for my class. The problem with this is that CommentChecker generates warnings telling me that the constructor is missing the comments.

I tried to modify the program to detect this special case and ignore it but I'm stuck, I already tried with IsDefined(typeof(CompilerGeneratedAttribute), true) but that did not work.

So in short, how can I detect the default constructor using reflection?

like image 908
Juan Zamudio Avatar asked Jul 06 '10 22:07

Juan Zamudio


3 Answers

If you're willing to dig a little into the IL, then you can get most of the way there.

First, assuming you have ConstructorInfo instance which you know to be parameterless, you can get the method body and the bytes for the method body like so (we'll start building an extension method to do this):

public static bool MightBeCSharpCompilerGenerated(
    this ConstructorInfo constructor)
{
    // Validate parmaeters.
    if (constructor == null) throw new ArgumentNullException("constructor");

    // If the method is static, throw an exception.
    if (constructor.IsStatic)
        throw new ArgumentException("The constructor parameter must be an " +
            "instance constructor.", "constructor");

    // Get the body.
    byte[] body = constructor.GetMethodBody().GetILAsByteArray();

You can reject any method bodies don't have seven bytes.

    // Feel free to put this in a constant.
    if (body.Length != 7) return false;

The reason will be obvious in the code that follows.

In section I.8.9.6.6 of ECMA-335 (Common Language Infrastructure (CLI) Partitions I to VI), it states CLS rule 21:

CLS Rule 21: An object constructor shall call some instance constructor of its base class before any access occurs to inherited instance data. (This does not apply to value types, which need not have constructors.)

This means that before anything else is done, the a base constructor must be called. We can check for this in the IL. The IL for this would look like this (I've put the byte values in parenthesis before the IL command):

// Loads "this" on the stack, as the first argument on an instance
// method is always "this".
(0x02) ldarg.0

// No parameters are loaded, but metadata token will be explained.
(0x28) call <metadata token>

We can now start checking the bytes for this:

    // Check the first two bytes, if they are not the loading of
    // the first argument and then a call, it's not
    // a call to a constructor.
    if (body[0] != 0x02 || body[1] != 0x28) return false;

Now comes the metadata token. The call instruction requires a method descriptor to be passed in the form of a metadata token along with the constructor. This is a four-byte value which is exposed through the MetadataToken property on the MemberInfo class (from which ConstructorInfo derives).

We could check to see that the metadata token was valid, but because we've already checked the length of byte array for the method body (at seven bytes), and we know that there's only one byte left to check (first two op codes + four byte metadata token = six bytes), we don't have to check to see that it's to a parameterless constructor; if there were parameters, there would be other op codes to push the parameters on the stack, expanding the byte array.

Finally, if nothing else is done in the constructor (indicating that the compiler generated a constructor that does nothing but call the base), a ret instruction would emitted after the call the metadata token:

(0x2A) ret

Which we can check like so:

    return body[6] == 0x2a;
}

It needs to be noted why the method is called MightBeCSharpCompilerGenerated, with an emphasis on Might.

Let's say you have the following classes:

public class Base { }
public class Derived : Base { public Derived() { } }

When compiling without optimizations (typically DEBUG mode), the C# compiler will insert a few nop codes (presumably to assist the debugger) for the Derived class which would cause a call to MightBeCSharpCompilerGenerated to return false.

However, when optimizations are turned on (typically, RELEASE mode), the C# compiler will emit the seven-byte method body without the nop opcodes, so it will look like Derived has a compiler-generated constructor, even though it does not.

This is why the method is named Might instead of Is or Has; it indicates that there might be a method that you need to look at, but can't say for sure. In other words, you'll never get a false negative but you still have to investigate if you get a positive result.

like image 66
casperOne Avatar answered Nov 14 '22 21:11

casperOne


There is no way to detect automatically generated default constructors through metadata. You can test this by creating a class library with two classes, one with an explicit default constructor, and one without. Then run ildasm on the assembly: the metadata of the two constructors is identical.

Rather than try to detect generated constructors, I would simply change the program to allow missing documentation on any default constructor. Most documentation generation programs, like NDoc and SandcastleGUI, have an option to add standard documentation to all default constructors; so it's really not necessary to document them at all. If you have an explicit default constructor in your code, you can put three slashes (///) above the constructor - nothing else - to disable the Visual Studio warning about missing documentation.

like image 39
David Nelson Avatar answered Nov 14 '22 21:11

David Nelson


The following code will return information on any parameterless constructors in your type:

var info = typeof(MyClass).GetConstructor(new Type[] {});

I do not know of a way of differentiating between a default constructor and an explicitly specified parameterless constructor.

A possible workaround for your CommentChecker issue would be to explicitly create the parameterless constructor where one is required and comment it appropriately.

like image 33
Ben Aston Avatar answered Nov 14 '22 23:11

Ben Aston