1

how can I get all the types that been used in specific type?

Example for class "MyClass":

[MyAttribute(new OtherType(TestEnum.EnumValue1))]
public class MyClass:MyOtherClass
{
    public Type MyType { get; set; }
    public string MyString { get; set; }

    private DateTime MyDateTime;
    [OtherAttribute()]
    public int MyMethod(double doubleNumber, float floatNumber)
    {
        justMyClass myJustClass = new justMyClass();
        return (int)doubleNumber / (int)floatNumber + myJustClass.Count();
    }
}

I want to get the types: MyAttribute, OtherType, TestEnum, MyClass, MyOtherClass, Type, string, DateTime, OtherAttribute, int, double, float and justMyClass.

Is there any way to do it?

user436862
  • 867
  • 2
  • 15
  • 32

1 Answers1

6
[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.

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251