show_menu logo_rd

Writing a ReSharper Plugin: Search and Navigation

0

Continuing the series of our Writing a ReSharper Plugin series (Quick Fixes, Problem Analyzers and Auto-completion), this article describes how the search and navigation feature was implemented for members accessed through Reflection API. Implementation is based on two sections from the official documentation (Reference Providers, Navigation) and a little debugging of ReSharper assemblies using dotPeek. (By the way, did you know that dotPeek 1.2 allows debugging third party assemblies?) As official documentation covers almost every point of implementation, I will try not to duplicate the existing information and cover only specific points of interest.

Writing_a_ReSharper_Plugin

References

According to the documentation, references are used to create a reference from one node in the PSI abstract syntax tree (AST) to another. Basically, ReSharper uses references in many different ways. You can either press Ctrl+Click or start a rename refactoring – references mechanism is involved under the hood. Because these features are so widely used, it was quite obvious for me what feature I wanted to see next in the ReSharper plugin for Reflection.

In my previous article about problem analyzers, I explained how to validate if the access to a member using Reflection is valid. And you know what? These validations already contained every bit of information I needed. So a small modification (lines 5-7, 15-17) in the ReflectionMemberValidatorBase class allowed me to create required references.

if (resolvedMembers.Length > 1)
            {
                if (!ProcessAmbigiousMembers(resolvedMembers, out resolvedMember))
                {
                    nameArgument.UserData.PutData(ReflectedMemberReference.Key,
                    new ReflectedMemberReference(nameArgument,
                        new ResolveResultWithInfo(ResolveResultFactory.CreateResolveResult(resolvedMembers), ResolveErrorType.MULTIPLE_CANDIDATES), resolvedType.TypeElement));
                    return new AmbigiousMemberMatchError(nameArgument, ExpectedMemberType, GetAmbigiuityResolutionSuggestion());
                }
            }
            else
            {
                resolvedMember = resolvedMembers[0];

                nameArgument.UserData.PutData(ReflectedMemberReference.Key,
                    new ReflectedMemberReference(nameArgument,
                        new ResolveResultWithInfo(ResolveResultFactory.CreateResolveResult(resolvedMember), ResolveErrorType.OK), resolvedType.TypeElement));
                return ValidateBindingFlags(resolvedType.TypeElement, resolvedMember, invocation)
                       ?? ValidateCore(resolvedType.TypeElement, resolvedMember, invocation);
            }

            return null;

The implementation of IReferenceFactory is straightforward.

public IReference[] GetReferences(ITreeNode element, IReference[] oldReferences)
        {
            if (element is ICSharpArgument)
            {
                var argument = ((ICSharpArgument) element);
                if (argument.Expression != null)
                {
                    var reference = argument.Expression.UserData
                    .GetData(ReflectedMemberReference.Key);
                    if (reference != null)
                    {
                        return new IReference[]
                        {
                            reference
                        };
                    }
                }
            }
            return new IReference[0];
        }

ReflectedMemberReference is a custom reference implementation inherited from TreeReferenceBase<>. Implementation of the BindTo method allows our references to participate in rename refactorings. The logic is very simple: if the declared element was renamed, a new StringLiteralExpression needs to be created for the corresponding argument.

ElementProblemAnalyzer vs. Daemon Stage

My previous validation logic was based on ElementProblemAnalyzer. Actually, all problem analyzers are executed within a separate daemon stage. Because it was not possible to configure the order of execution, I needed to refactor the implementation to be daemon-stage-based instead. This was done to support Reflection references in CollectUsagesStage. So, ReSharper will not highlight type members accessed via Reflection as unused anymore.
ELEKS_Writing_ReSharper_Plugin4_1

Search and Navigation

Associating IReference with specific ITreeNode enables search functionality without additional steps. Unfortunately, it is not enough for members accessed via reflection. As you know, it is possible to access both private and internal members using reflection API. Of course, the search scope for such references is narrowed down to the exact class for private members or to the parent assembly for internal members. It took me a while to find a couple of ways to influence search parameters.

The first way is to implement the ISearchGuru interface. The implementation provides the search engine with hints on whether a particular file should be processed. The search guru doesn’t influence the search scope. The second way is to implement IDomainSepcificSearcherFactory. It has a bunch methods to be implemented but most of them may return null. The method used to find usages of a type member is GetDeclaredElementSearchDomain. Search domains of multiple factories are united. The implementation is straightforward.

public ISearchDomain GetDeclaredElementSearchDomain(IDeclaredElement declaredElement)
        {
            return _searchDomainFactory.CreateSearchDomain(declaredElement.GetPsiServices().Solution, false);
        }

Of course, the logic needs to be improved, as currently the search domain is extended to the whole solution. At least an option that would allow turning off this feature should be added to the options page.

Occurrence Implementation

The occurrence stands for reference presentation and navigation from reference. The IOccurenceProvider interface needs to be implemented for custom occurrences. ReSharper default implementation creates the ReferenceOccurence instance for references inherited from TreeReferenceBase. Even though it has almost all of the functionality required, I missed the Filtering of the found results feature. The filtering behind the hood is based on the OccurrenceKind enum pattern. So, I have created a custom occurrence for elements accessed with Reflection.

It turned out to be very simple to add an additional menu item for the filter, as all the configurations for the majority of toolbars and menus are stored in Actions.xml. So, it was only a matter of time to find the corresponding group-id. Also, it is possible to insert custom commands into Visual Studio menus.
Here is the actions configuration XML:

<actions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
         xmlns="urn:shemas-jetbrains-com:actions-xml">

  <action id="OccurenceBrowser.Filter.ShowReflection" type="checkbox" text="Show Reflection Usages"/>
  
  <insert group-id="OccurenceBrowser.Show" position="last">
    <action-ref id="OccurenceBrowser.Filter.ShowReflection"/>   
  </insert>
</actions>

And here is the result:
ELEKS_Writing_ReSharper_Plugin4_2

Demo

  • Navigation
    ELEKS_Writing_ReSharper_Plugin4_3
  • Navigation to member with ambiguous match
    ELEKS_Writing_ReSharper_Plugin4_4
  • Rename Refactoring
    ELEKS_Writing_ReSharper_Plugin4_5
  • Find usages
    ELEKS_Writing_ReSharper_Plugin4_6

Summary

An updated version of the plugin is packaged and published to the ReSharper plugins gallery. I would like to add that the more I dig into ReSharper infrastructure and code, the more I like it. It is really well designed and implemented. And this actually explains ReSharper popularity among .NET developers.

Yuriy Zanichkovskyy

Yuriy Zanichkovskyy is a .NET tech lead. For a long time he has been developing user interfaces and working with WPF, but now he is mainly engaged in Domain Specific language development. He is interested in compilers, static code analyzers and likes writing plugins for applications like MS VisualStudio and ReSharper. He loves pastel drawing, pole dancing and playing guitar. Yuriy has a lot of interesting ideas, and blogging is his motivation to start implementing them.

tags

Comments: 0