[MyAttribute(new OtherType(TestEnum.EnumValue1))]
That's not valid, you have to have a constant in attribute constructors. That matter aside, most of this is easy, if rather long-winded.
You can call typeof(MyClass).CustomAttributes.Select(ca => ca.AttributeType)
to get the types of attributes, typeof(MyClass).GetFields().Select(fi => fi.FieldType)
to get the types of fields, and so on. Union
those together and you'll have all the types from the signatures and attributes.
Getting justMyClass
for MyMethod
is trickier (getting double
and float
is easy, they'll come up when you did typeof(MyClass).GetMethods().SelectMany(mi => mi.GetParameters()).Select(pa => pa.ParameterType)
and likewise int
for the return type).
In a debug build we can expect the method to compile to something like this:
.method public hidebysig instance int32 MyMethod (float64 doubleNumber, float32 floatNumber) cil managed
{
.custom instance void Temp.Program/OtherAttribute::.ctor() = (
01 00 00 00
)
.maxstack 2
.locals init (
[0] class Temp.Program/justMyClass myClass,
[1] int32 ret
)
nop
newobj instance void Temp.Program/justMyClass::.ctor()
stloc.0
ldarg.1
conv.ovf.i4
ldarg.2
conv.ovf.i4
div
stloc.1
ret
}
And we can get the int
and the justMyClass
simply enough if we'd done:
typeof(MyClass)
.GetMethods()
.Select(mi => mi.GetMethodBody())
.Where(mb => mb != null)
.SelectMany(mb => mb.LocalVariables)
.Select(lv => lv.LocalType)
However, with a release build we'd expect it to be compiled to something more like:
.method public hidebysig instance int32 MyMethod (float64 doubleNumber, float32 floatNumber) cil managed
{
.custom instance void Temp.Program/OtherAttribute::.ctor() = (
01 00 00 00
)
.maxstack 8
newobj instance void Temp.Program/justMyClass::.ctor()
pop
ldarg.1
conv.i4
ldarg.2
conv.i4
div
ret
}
And because the result of the call to new justMyClass()
isn't used, it's not stored in a local for debugging purposes. Even a value that was used might just be used from its position on the stack rather than stored in the locals array, so even more realistic code might result in types being missed. Instead you'd have to actually disassemble the call. A start on such a method is:
private static IEnumerable<Type> GetUsedTypes(Type type, MethodInfo mi)
{
var body = mi.GetMethodBody();
if(body == null) // not managed code
yield break;
var cil = body.GetILAsByteArray();
for(int idx = 0; idx < cil.Length; ++idx)
{
switch(cil[idx])
{
case 0x73: // newobj
int token = cil[++idx];
token |= (int)cil[++idx] << 8;
token |= (int)cil[++idx] << 16;
token |= (int)cil[++idx] << 24;
yield return type.Module.ResolveMethod(token).DeclaringType;
break;
/* TODO: Other opcodes that deal with types */
}
}
}
This case above only deals with newobj
, by examining the constructor that follows, and isn't examining generic type parameters etc. The method would have to be extended to also deal with call
, callvirt
and so on, and to make sure it didn't see a 0x73
that was actually part of another token, etc. That's quite a bit of work, but the above suffices to show that it can work (and does indeed give the correct answer in this case).
Even still though, there are three cases where this won't be quite as expected.
One is looked for in the code; if there's an internalcall
you won't get any method body to examine.
Another is that you might be surprised in more complicated versions of cases like:
public void HasNoType()
{
if(false)
throw new Exception("Impossible");
}
While the source has a bool
, a string
and an Exception
when compiled the dead code will be removed so a debug compile might have a bool
(so we can see the false
in a debugger) and a release compile will have nothing at all, just an immediate ret
.
A further case again is:
public bool CheckIsInRangeWhenAlreadyWeKnowSizeIsNotNegative(int idx, int size)
{
return (uint)idx < (uint)size;
}
From first glance at the source this seems to be using uint
, but it isn't really. Instead the CIL produced is:
.method public hidebysig instance bool CheckIsInRangeWhenAlreadyWeKnowSizeIsNotNegative (int32 idx, int32 size) cil managed
{
.maxstack 8
ldarg.1
ldarg.2
clt.un
ret
}
In CIL 32-bit integers on the stack are not quite Int32
or UInt32
, but differ in behaviour depending on what operations are done with them, so there's no conversion and nowhere in the code that uint
is used, but they're just compared in an unsigned way, to which the closest thing in C# is to convert to uint
and then compare. Hence in examining the method body you won't find a uint
in there, unless you go further in disassembly and into actually decompiling and see that the only way to express this in C# is by using uint
.
So, while getting all the types that are part of the signatures and attributes is easy enough (if rather convoluted), getting all the locally-used types is either very hard or impossible depending on just what you consider a locally used type.