Writing a ReSharper Plugin: Problem Analyzers

Yuriy Zanichkovskyy on
.NET Tech Lead at ELEKS

My previous article described how various quick fixes are implemented using ReSharper extensibility API. Both examples provided were based on predefined ReSharper problem highlights. However, most plugins will definitely need to define custom highlights. In this article, I will describe in detail what highlighting is, its role in ReSharper infrastructure, and procedure of registering custom highlighting. And last but not least, I will explain how to analyze source code, associate highlighting with source code, and eventually provide quick fixes for highlights.
resharper_8_release

Problem Highlighting

Actually, highlighting is just an implementation of the IHighlighting interface that provides text being displayed in a tooltip. In most cases, implementation contains everything that is needed for quick fixes to be executed (for example, AccessRightsError contained ReferenceExperession whose access rights were violated; for more information, see previous article).

Before highlighting can be used, it should be registered in ReSharper infrastructure by using StaticSeverityHighlightingAttribute. Based on the specified Severity, highlighting will be displayed in code editor appropriately. The image below illustrates highlighting registered with the Severity.ERROR type.
Writing a ReSharper Plugin2_1
The drawback of static severity is that it cannot be configured by the users the based on their own needs. It is possible to register highlighting with ConfigurableSeverityHighlighting.

    [ConfigurableSeverityHighlighting("ReflectionAmbigiousMatch", "CSHARP")]
    public class AmbigiousMemberMatchError : ReflectionHighlightingBase, IHighlighting
    {
        //implementation goes here
    }

Here, it is important not to forget about registration of configurable highlighting using assembly-level attribute for the same ConfigurableSeverityId.

[assembly: RegisterConfigurableSeverity("ReflectionAmbigiousMatch", null, "CodeInfo", "Some title", "Other title", Severity.WARNING,  false)]
Highlighting registered as configurable will contain a menu that allows specifying severity level based on your needs.

Writing a ReSharper Plugin2_2
Writing a ReSharper Plugin2_3

You may wonder how ReSharper actually knows where to paint the highlighting. Actual range in the document is specified during highlighting registration and will be described in the next chapter Problem Analyzers. Also, there is a separate kind of highlighting – IhighlightingWithRange that itself can calculate and provide DocumentRange.

Whenever you need to render something completely different for your highlighting, you may implement ICustomStyleHighlighting. For examples, I recommend using dotPeek to dive into disassembled ReSharper code that is luckily not obfuscated.

Problem Analyzers

It is possible to register highlighting for the code issue in ReSharper infrastructure in several ways:

  • Custom daemon stage implementation. Such approach offers better flexibility but it is a bit tedious to implement. It is well explained in Hadi Hariri’s article and there is a good sample located in the ‘SDK\Samples\CyclomaticComplexity\’ folder.
  • Class inherited from ElementProblemAnalyzer. Allows implementing code issue analysis for specific AST nodes. Basically, all problem analyzers are invoked from daemon stage called CSharpErrorStage.

This article describes implementation of ElementProblemAnalyzer for invocation of Reflection Type methods, such as GetMember, GetProperty, MakeGenericType, argument static verification. As analyzer deals only with a single node type (IInvocationExpression), it is better to use generic counterpart – ElementProblemAnalyzer<T>. ElementProblemAnalyzer has a single method to be overridden – the Run method with the following signature:

void Run(ITreeNode element, ElementProblemAnalyzerData data, IHighlightingConsumer consumer); 

Problem analyzers are discovered by CSharpErrorStage based on ElementProblemAnalyzerAttribute that serves as a contract to analyzer inputs (AST nodes only specified in attribute node types will be passed to the Run method) and result outputs (highlights that are reported to highlighting consumer).

ReReflection plugin implements ElementProblemAnalyzer to validate reflection API usages in the following way:

[ElementProblemAnalyzer(new[] { typeof(IInvocationExpression) }, 
        HighlightingTypes = new[] { typeof(ReflectionMemberNotFoundError), typeof(IncorrectMakeGenericTypeHighlighting) })]
    public class ReflectionProblemsAnalyzer : ElementProblemAnalyzer<IInvocationExpression>
    {
        protected override void Run(IInvocationExpression element, ElementProblemAnalyzerData data, IHighlightingConsumer consumer)
        {
            //Validation logic comes here
        }
    }

Actual highlights are registered in IHighlightingConsumer with the ConsumeHighlighting method or any extension method that is available. For example, I have used the AddHighlighting method from CSharpHighlightingConsumerExtension.

When working with AST node, usually, some type information is required. It can be obtained by using the Resolve method. For example, if we need to get a method that is being invoked, we need to invoke the expression.InvocationExpressionReference.Resolve() method. In case of successful resolution, ResolveResultWithInfo will contain type information in the DeclaredElement property. For method invocations, it will be an implementation of the IMethod interface. The easiest way to match a specific method with the corresponding CLR MethodInfo is by using XmlDocId. It is guaranteed to be unique for every type and member.

After the method is identified, it is possible to verify arguments passed using IExpression.ConstantValue. Of course, validation works only if arguments are specified as constant values =).

The most complex part of the logic is reflected type identification. Currently, only a part of functionality is implemented and it deals only with method invocations exactly after the typeof expression. In the next versions, I will support resolution based on the Type local variables. Another possible case is when the GetType method is used. In such case, validation logic will be slightly different because we will need to check inherited classes.

The reflected type is resolved using the following method:

        private ReflectedTypeResolveResult ResolveReflectedType(IInvocationExpression invocationExpression)
        {
            var referenceExpression = invocationExpression.InvokedExpression as IReferenceExpression;

            if (referenceExpression != null)
            {
                var typeOfExpression = referenceExpression.QualifierExpression as ITypeofExpression;
                if (typeOfExpression != null)
                {
                    var type = typeOfExpression.ArgumentType.GetTypeElement<ITypeElement>();
                    if (type == null)
                    {
                        return ReflectedTypeResolveResult.NotResolved;
                    }

                    return new ReflectedTypeResolveResult(type, ReflectedTypeResolution.Exact); 
                }
                var methodInvocationExpression = referenceExpression.QualifierExpression as IInvocationExpression;
                if (methodInvocationExpression != null && IsReflectionTypeMethod(invocationExpression, "MakeGenericType"))
                {
                    var resolvedType = ResolveReflectedType(methodInvocationExpression);
                    if (resolvedType.ResolvedAs == ReflectedTypeResolution.Exact)
                    {
                        return new ReflectedTypeResolveResult(resolvedType.TypeElement, ReflectedTypeResolution.ExactMakeGeneric);
                    }
                }
            }

            return ReflectedTypeResolveResult.NotResolved;
        }

The resolution logic will support more cases in upcoming versions of the plugin.

ReReflection Plugin

This section gives a short overview of features implemented in the plugin so far.

Analysis for Methods

Validations for methods are registered in ReflectionValidatorsRegistry.

    public static class ReflectionValidatorsRegistry
    {
        private static readonly Type _T = typeof (object);
        private static readonly IDictionary<string, Func<IMethod, ReflectionTypeMethodValidatorBase>> _registeredValidators = 
            new Dictionary<string, Func<IMethod, ReflectionTypeMethodValidatorBase>>
            {
                //MakeGenericType
                { Methods.Of<Func<Type[], Type>>(() => _T.MakeGenericType).XmlDocId(), (m) => new MakeGenericTypeValidator(m) },

                //GetProperty overloads
                { Methods.Of<Func<string, PropertyInfo>>(() => _T.GetProperty).XmlDocId(), (m) => new GetPropertyMethodValidator(m) },
                { Methods.Of<Func<string, BindingFlags, PropertyInfo>>(() => _T.GetProperty).XmlDocId(), (m) => new GetPropertyMethodValidator(m, 1) },
                //GetField
                { Methods.Of<Func<string, FieldInfo>>(() => _T.GetField).XmlDocId(), (m) => new GetFieldMethodValidator(m) },
                { Methods.Of<Func<string, BindingFlags, FieldInfo>>(() => _T.GetField).XmlDocId(), (m) => new GetFieldMethodValidator(m, 1) },
                //GetMethod
                { Methods.Of<Func<string, MethodInfo>>(() => _T.GetMethod).XmlDocId(), (m) => new GetMethodMethodValidator(m) },
                { Methods.Of<Func<string, BindingFlags, MethodInfo>>(() => _T.GetMethod).XmlDocId(), (m) => new GetMethodMethodValidator(m, 1) }
            };

        public static ReflectionTypeMethodValidatorBase GetValidator(IMethod method)
        {
            Func<IMethod, ReflectionTypeMethodValidatorBase> validatorFactory;
            if (_registeredValidators.TryGetValue(method.XMLDocId, out validatorFactory))
            {
                return validatorFactory(method);
            }

            return null;
        }
    }

The implementation can be easily extended to support all other Type methods.

Highlights

  • AmbigiousMemberMatchError – For cases when there are several method overloads with the same name in the reflected type.
    Writing a ReSharper Plugin2_4
  • BindingFlagsCanBeSkippedWarning – If BindingFlags specified as argument exactly matches the default value used by Reflection.
    Writing a ReSharper Plugin2_5
  • IncorrectBindingFlagsErrorBindingFlags specified for the current type member are incorrect. For example, BindingFlag.Static is missed for a static member.
    Writing a ReSharper Plugin2_6
  • IncorrectMakeGenericTypeHighlighting – Highlighting for MakeGenericType misuse.
    Writing a ReSharper Plugin2_7
  • ReflectionMemberNotFoundError – Member with the specified name cannot be found in the reflected type.
    Writing a ReSharper Plugin2_8

Quick Fixes

Quick fixes are based on the implemented highlights.

  • CorrectBindingFlagsQuickFix – Is based on IncorrectBindingFlagsError
  • RemoveBindingFlagsQuickFix – Is based on BindingFlagsCanBeSkippedWarning

To see how they work, watch this video.

Conclusion

When eventually verification logic was implemented, I was a bit upset that I did not have such plugin before. I hope that sometime in the future this functionality will be included in ReSharper by default. As usual, the latest plugin source code can be found at GitHub. Any issue encountered might also be registered there.

tags

Comments