Accessing attribute info from DTE
Asked Answered
T

2

29

I have coded something like the following:

[Attrib(typeof(MyCustomType))]
public class TargetType
{
  // .....
}

I want to use EnvDTE to get a reference to the CodeElement referenced by the typeof. I know how to get a reference to the attribute argument, and I can use Value, but that gives me the string typeof(MyCustomType).

If I use Value, I have to break down the string and then try to find the type, which gets hairy if there are two types with the same name but different namespaces.

Is there an easier way to do this?

Thimerosal answered 1/3, 2012 at 17:15 Comment(4)
so how do you get a reference to the attribute argument?Formfitting
Have you looked into Roslyn? It should offer the features you're looking for.Niece
Have you checked the attribute's FullName property?Bautram
Possible duplicate of Getting the CodeClass from argument inside CodeAttribute? - it looks like you are limited to just the Value, EnvDTE won't parse it for you.Uncleanly
B
5

Is there an easier way to do this?

No, I don't think so, atleast for a <= VS2013, it seems that the CodeAttributeArgument doesn't go any further, which is shame. They should've released CodeAttributeArgument2 that has Value as CodeExpr :\..

If you use >=VS2014, you can get access to Roslyn, and it should become easier - don't know exactly how you can access roslyn inside VS extension, have to wait and see.

In order to get attributes, you can use VS helper:

public List<CodeElement> GetAllCodeElementsOfType(
    CodeElements elements, 
    vsCMElement elementType, 

    bool includeExternalTypes)
{
    var ret = new List<CodeElement>();

    foreach (CodeElement elem in elements)
    {
        // iterate all namespaces (even if they are external)
        // > they might contain project code
        if (elem.Kind == vsCMElement.vsCMElementNamespace)
        {
            ret.AddRange(
                GetAllCodeElementsOfType(
                    ((CodeNamespace)elem).Members, 
                    elementType, 
                    includeExternalTypes));
        }

        // if its not a namespace but external
        // > ignore it
        else if (elem.InfoLocation == vsCMInfoLocation.vsCMInfoLocationExternal && !includeExternalTypes)
            continue;

        // if its from the project
        // > check its members
        else if (elem.IsCodeType)
        {
            ret.AddRange(
                GetAllCodeElementsOfType(
                    ((CodeType)elem).Members, 
                    elementType, 
                    includeExternalTypes));
        }

        if (elem.Kind == elementType)
            ret.Add(elem);
    }
    return ret;
}

Original source: https://github.com/PombeirP/T4Factories/blob/master/T4Factories.Testbed/CodeTemplates/VisualStudioAutomationHelper.ttinclude

In a meanwhile, you could use backtracking solution, this is not nice, but it should work, haven't tested it exactly 100%. The basic idea is to start tracking backwards from the class, and keep track of different namespaces/usings that arein the path of a class. This tries to simulate pretty much what a real compiler would do, if it's going to resolve a type:

 var solution = (Solution2) _applicationObject.Solution;
var projects = solution.Projects;
var activeProject = projects
    .OfType<Project>()
    .First();

// locate my class.
var myClass = GetAllCodeElementsOfType(
    activeProject.CodeModel.CodeElements,
    vsCMElement.vsCMElementClass, false)
    .OfType<CodeClass2>()
    .First(x => x.Name == "Program");

// locate my attribute on class.
var mySpecialAttrib = myClass
    .Attributes
    .OfType<CodeAttribute2>()
    .First();



var attributeArgument = mySpecialAttrib.Arguments
    .OfType<CodeAttributeArgument>()
    .First();

string myType = Regex.Replace(
    attributeArgument.Value, // typeof(MyType)
    "^typeof.*\\((.*)\\)$", "$1"); // MyType*/

var codeNamespace = myClass.Namespace;
var classNamespaces = new List<string>();

while (codeNamespace != null)
{
    var codeNs = codeNamespace;
    var namespaceName = codeNs.FullName;

    var foundNamespaces = new List<string> {namespaceName};

    // generate namespaces from usings.
    var @usings = codeNs.Children
        .OfType<CodeImport>()
        .Select(x =>
            new[]
            {
                x.Namespace,
                namespaceName + "." + x.Namespace
            })
        .SelectMany(x => x)
        .ToList();

    foundNamespaces.AddRange(@usings);

    // prepend all namespaces:
    var extra = (
        from ns2 in classNamespaces
        from ns1 in @usings
        select ns1 + "." + ns2)
        .ToList();

    classNamespaces.AddRange(foundNamespaces);
    classNamespaces.AddRange(extra);

    codeNamespace = codeNs.Parent as CodeNamespace;
    if (codeNamespace == null)
    {
        var codeModel = codeNs.Parent as FileCodeModel2;
        if (codeModel == null) return;

        var elems = codeModel.CodeElements;
        if (elems == null) continue;

        var @extraUsings = elems
            .OfType<CodeImport>()
            .Select(x => x.Namespace);

        classNamespaces.AddRange(@extraUsings);
    }
}

// resolve to a type!
var typeLocator = new EnvDTETypeLocator();
var resolvedType = classNamespaces.Select(type =>
        typeLocator.FindTypeExactMatch(activeProject, type + "." + myType))
    .FirstOrDefault(type => type != null);

You need EnvDTETypeLocator too.

For VS2015, an example of roslyn integration can be found from here: https://github.com/tomasr/roslyn-colorizer/blob/master/RoslynColorizer/RoslynColorizer.cs

It'll definitely be A lot easier than it is with current CodeModel.

Bellabelladonna answered 31/12, 2014 at 23:3 Comment(0)
E
-2

You can try to use a way described Get all methods that are decorated with a specific attribute using T4/EnvDTE

Etan answered 27/10, 2014 at 18:33 Comment(1)
This could use a good deal more specificity; at minimum, briefly summarize the relevant parts. Just looking at it I couldn't tell what you were referring to.Anglicanism

© 2022 - 2024 — McMap. All rights reserved.