How do I get an IDeclaredType from an IClrDeclaredElement with the Resharper SDK

404 Views Asked by At

I'm writing a navigation plugin for Resharper, and my situation is that I have a list of IDeclaredElement that I obtained from doing

var declaredElements = context.GetData(DataConstants.DECLARED_ELEMENTS)

This element is the element that the user has their mouse cursor on.

What I'd like to do is to get the IDeclaredType of the declared element, including any type parameters it might have, in the case it's a generic type.

The resharper SDK documentation is quite light when it comes to the type system, and doesn't really explain the relationship between the various types at all.

I've hunted around other plugins to try and find examples of this, but have come up empty. I've checked every Util and Extension class there is to see if there's a method somewhere that gives me what I want, but there's nought.

About the only thing I have found was this:

declaredElements.First().GetSuperTypes()

which returns the type hierarchy, excluding the current type. Useful, but not what I'm looking for.

Does anyone have any experience with this API or understand how it works? I'd love an answer that explained the relationships between the types a little more.

My understanding of it, in brief:

  • Types with Declared in the name (IDeclaredElement, IDeclaredType) appear to refer to physical code elements.
  • IType appears to be a top level interface for all types, that doesn't correspond to physical code elements
  • I'm unclear on the meaning of types with Element in the name (ITypeElement, IDeclaredElement), perhaps it's referring to AST elements.

I'd love some clarification on this.

2

There are 2 best solutions below

2
On BEST ANSWER

I've added an issue to get this updated in the documentation: https://github.com/JetBrains/resharper-devguide/issues/4

I'll try and provide a potted explanation here.

The ITreeNode type hierarchy defines the abstract syntax tree of your code. This provides a lot of information, but is very low-level - it maps directly to the raw text of the code. It also misses some higher level information. For example, if I want to get all type members for a class declaration, I could walk the AST for the class, and collect all appropriate tree nodes, but then I'd also have to process partial classes, and the AST provides no information for locating other parts of the class. Similarly, if I see the class declaration public class Foo : Bar, I would have to do manual resolution of the Bar base type.

The IDeclaredElement type hierarchy is essentially the semantic view of the syntax tree. At its simplest level, a declared element is "something that has declarations". This can be a class declaration, or a method declaration, or something not even related to code - HTML elements, CSS classes and even colours and file system paths (this is why it's called "element" - it needs a name that can apply to a lot of different things).

For example, CLR types are represented with the ITypeElement interface, which derives from IDeclaredElement. It provides methods and properties for getting at the declared elements for the target type's methods, properties, constructors, etc. So, it is (almost) possible to provide a semantic view of a CLR source project solely in terms of declared elements. Almost, but not quite.

Declared elements have the GetDeclarations method that provides IDeclaration syntax tree nodes that are the declarations for the declared element. Similarly, the IDeclaration node provides the DeclaredElement property to be able to get a declared element from a node.

Furthermore, ReSharper has a very powerful mechanism called references, that allow a tree node to have an outgoing reference that will resolve to a declared element (it can also fail to resolve, which is an error such as using a method that isn't yet written, or it can resolve to more than one element, such as using a method without qualifying which overload it is). These references can be applied to any node, such as a variable name referring back to the variable declaration, or the Bar in public class Foo : Bar having a reference to the declared element of Bar (from which it's possible to get the IDeclaration and the source code of Bar).

This provides an impressive set of features - a syntactic view of the code file, a semantic view of the code declarations, and references to join everything together, but this doesn't cover everything. A declared element provides a semantic view of a declared thing, but isn't intended to represent all usage scenarios.

Specifically (looking at CLR types), it can't represent usage of a type as an array, pointer, or closed generic type. ITypeElement can provide a semantic view of the class Foo or Bar<T>, but it can't represent Foo[], or Bar<Quux>.

Declared elements need to be able to model these usage scenarios as base classes, method signatures, etc. In order to do so, the derived declared elements (such as ITypeElement) use an additional interface hierarchy to represent this "type system" information. This hierarchy depends on the language being analysed. For CLR types, it's the IType hierarchy, for JavaScript, it's IJavaScriptType.

This IType is additional information, rather than a replacement for the declared element's semantic view. The IType can return a symbol table of all type members, but doesn't provide accessors in the same way that ITypeElement does. Instead, (and depending on what's being modelled) an IType is essentially a wrapper around a declared element and an instance of ISubstitution, which provides substitutions for generic type parameters (an array is represented as the System.Array type, with an underlying element type, which is itself an IType, as it may be a closed generic, or another array). The substitution can also be the empty substitution, which doesn't substitute anything, allowing for types represented as open generics, or types that aren't generic at all. The IDeclaredType interface is an IType that refers to a declared element.

Aside: Resolving references actually resolves to a declared element and an ISubstitution, again, to model generics. When resolving a reference for a method declaration signature, you need to know both that it's an IList<T>, and what T is.

In order to get an IType instance, you either need to get one from an existing declared element (method signature, base class, etc.) or by creating it with TypeFactory.CreateType. You'll most likely need to specify an ISubstitution, too, if it's a generic type. You can also get at a bunch of common, "predefined" types, via:

var type = psiModule.GetPredefinedType(context).String; 

You can use these types to pass into one of the TypeFactory.CreateType methods to act as type parameters to the ITypeElement you also pass in.

So, the upshot is, we declare a class in source, this gives us ITreeNode, IDeclaration and ITypeDeclaration. We can use IDeclaration, or resolve references to get the semantic view of this declaration, IDeclaredElement, with ITypeElement being the derived interface representing the class. CLR based declared elements use IType to represent type usage, such as base classes, which might need to be a closed generic, or method parameters which might be open generics, or arrays. IDeclaredType is a type usage that can get us back to a declared element. And types are often internally represented with a declared element and an ISubstitution, which can fill in any generic parameters, or be the ID substitution, for when there are no generic parameters. And finally, you can get an IType using TypeFactory.CreateType or using the properties on PredefinedType.

0
On

Check out this code:

void Do(IDataContext dataContext)
{
  foreach (var reference in dataContext.GetData(DataConstants.REFERENCES))
  {
    var resolveResultWithInfo = reference.Resolve().Result;
    var typeElement = resolveResultWithInfo.DeclaredElement as ITypeElement;
    if (typeElement != null)
    {
      var substitution = resolveResultWithInfo.Substitution;
      var declaredType = TypeFactory.CreateType(typeElement, substitution);
    }
  }
}