Introduction.
It is almost impossible to find a .NET developer who does not use ReSharper. The reason is obvious - ReSharper is always step ahead of Visual Studio in its refactoring, auto-completion and code-generation features. But not many know that since the 5th version ReSharper has a set of extensions points that allow developers to create their own productivity tools.
In this series of articles we will deal with the creation of useful ReSharper plugin for code that uses .NET Reflection API.
Starting up.
The first step you need to do to start ReSharper plugin development is to install ReSharper SDK. Installer adds project templates to Visual Studio and samples that cover most extensions points available. Samples may be found in “Program Files (x86)\JetBrains\ReSharper\v8.0\SDK\Samples\” folder. Although ReSharper extensibility points, base classes and interfaces lack of documentation, samples source code is well written and easy to understand so it will definitely give you a clue.
Implementing a ReSharper QuickFix.
QuickFix is a set of executable actions that modify a part of code where cursor is located. It is always associated with some kind of highlighting (custom highlightings will be described in the subsequent article). There are pretty a lot of quick fixes available in ReSharper and they are frequently used by developers (e.g. Make property public or internal, check for null some reference, optimize imports, etc.).
IQuickFix interface is quite simple and self-descriptive.
{
IEnumerable<IntentionAction>
CreateBulbItems();
bool
IsAvailable([NotNull] IUserDataHolder cache);
}
CreateBulbItems is responsible for reporting menu items that will be available for execution. IsAvailable used to report whether quick fix is available in current context. In most cases it is enough to inherit from QuickFixBase class. QuickFix needs to have a public constructor that accepts any type that implements IHighlighting interface as an argument. The last thing that needs to be done to make things working is to mark implemented class with QuickFixAttribute.
public class SampleQuickFix : QuickFixBase
{
{
}
protected override Action<ITextControl>
ExecutePsiTransaction(ISolution solution, IProgressIndicator progress)
{
return null;
}
public override string Text
{
get
{
return "Sample quick fix";
}
}
public override bool IsAvailable(IUserDataHolder cache)
{
return true;
}
}
Quick fixes that will be described in current article use highlightings that are already reported by ReSharper problem analyzers:
- AccessRightsError – is reported when you are trying to access internal or private property in scope where it is not allowed.
- NotResolvedError – for cases where reference expression couldn’t be resolved.
"Use Reflection" QuickFix.
It is a known fact that you may violate encapsulation principle by using Reflection to access private/internal fields, properties and methods. When using Reflection in such case you should be aware that you may leave an object in inconsistent state and it may behave as it was not designed to. So be aware!
As for me I can hardly remember any project where we didn’t need to access some internal methods and properties. Just as a quick example from previous project – we needed to modify default WPF DataGrid behavior on column header click so that instead of sorting it selects all cells belonging to that column. First approach with using public APIs failed as it was too slow. After that we have found that DataGrid uses internal methods for selection of large regions. And here Reflection came to rescue.
The quick fix implemented helps to generate Reflection code to access specific class member. You might remember that it is quite easy to miss some required BindingFlags, and you will see that your code doesn’t work only during program execution.
Before diving into implementation details you may watch the video how “Use Reflection” quick fix works.
Although implementation code is a bit verbose for first example, it is easy to understand what happens there, especially if you have basic understanding of what AST is (ReSharper works with code representation in form of PSI, changes to the tree are mirrored in the editor at once).
public UseReflectionQuickFix(AccessRightsError error)
{
_error = error;
_declaredElement =
error.Reference.CurrentResolveResult.DeclaredElement;
_languageForPresentation =
error.Reference.GetTreeNode().Language;
}
Public constructor accepts AccessRightsError highlighting with reference node, representing access to member that violates access rights.
The main implementation resides in ExecutePsiTransaction method. The implementation handles different corner cases:
- Reflection runtime invocation returned value is always object. I wanted the quick fix to generate correct C# code thus I have added casting support to property type value.
- Assignment operation treated separately.
- When invoking a method - array of arguments needs to passed to InvokeMember method.
protected override Action<ITextControl>
ExecutePsiTransaction(ISolution solution, IProgressIndicator progress)
{
var accessExpression = _error.Reference.GetTreeNode() as IExpression;
var replacementNode = accessExpression;
if (replacementNode == null)
return null;
var modifiers = _declaredElement as IModifiersOwner;
if (modifiers == null)
return null;
bool isAssign = replacementNode.Parent is IAssignmentExpression;
bool needsCasting = !isAssign && !(replacementNode.Parent is IExpressionStatement)
&& !_declaredElement.Type().IsVoid() &&
!_declaredElement.Type().IsObject();
if (replacementNode.Parent is IInvocationExpression || replacementNode.Parent is IAssignmentExpression)
{
replacementNode = (IExpression)replacementNode.Parent;
}
CSharpElementFactory factory = CSharpElementFactory.GetInstance(replacementNode,
applyCodeFormatter:true);
AddSystemReflectionNamespace(factory);
string flags = "BindingFlags.NonPublic";
if (modifiers.IsStatic)
{
flags += "| BindingFlags.Static";
}
else
{
flags += "| BindingFlags.Instance";
}
flags += "| " +
GetInvokeMemberBindingFlag(_declaredElement, isAssign);
IExpression instanceExpression = modifiers.IsStatic ?
factory.CreateExpression("null") : ((IReferenceExpression)accessExpression).QualifierExpression;
IExpression argsExpression = factory.CreateExpression("null");
if (isAssign)
{
argsExpression =
factory.CreateExpression("new object[] { $0 }",
((IAssignmentExpression)
replacementNode).Source);
}
if (replacementNode is IInvocationExpression)
{
var invocationExpression = (IInvocationExpression)replacementNode;
if
(invocationExpression.Arguments.Count != 0)
{
argsExpression =
CreateArrayCreationExpression(
TypeFactory.CreateTypeByCLRName(
"System.Object",
accessExpression.GetPsiModule(),
accessExpression.GetResolveContext()), factory);
var arrayCreationExpression =
argsExpression as IArrayCreationExpression;
foreach (var arg in
invocationExpression.ArgumentsEnumerable)
{
var initiallizer =
factory.CreateVariableInitializer((ICSharpExpression)
arg.Expression);
arrayCreationExpression.ArrayInitializer.AddElementInitializerBefore(initiallizer,
null);
}
}
}
var reflectionExpression = factory.CreateExpression("typeof($0).InvokeMember(\"$1\",
$2, null, $3, $4)",
((IClrDeclaredElement)_declaredElement).GetContainingType(),
_declaredElement.ShortName,
flags,
instanceExpression,
argsExpression);
if (needsCasting)
{
reflectionExpression =
factory.CreateExpression("($0)$1",
_declaredElement.Type(),
reflectionExpression);
}
replacementNode.ReplaceBy(reflectionExpression);
return null;
}
The following code is used to import “System.Reflection” namespace if it is not present in using directives.
private void
AddSystemReflectionNamespace(CSharpElementFactory factory)
{
var importScope = CSharpReferenceBindingUtil.GetImportScope(_error.Reference);
var reflectionNamespace = GetReflectionNamespace(factory);
if (!UsingUtil.CheckAlreadyImported(importScope, reflectionNamespace))
{
UsingUtil.AddImportTo(importScope,
reflectionNamespace);
}
}
private static INamespace
GetReflectionNamespace(CSharpElementFactory factory)
{
var usingDirective = factory.CreateUsingDirective("System.Reflection");
var reference = usingDirective.ImportedSymbolName;
var reflectionNamespace = reference.Reference.Resolve().DeclaredElement as INamespace;
return reflectionNamespace;
}
For PSI tree creation CSharpElementFactory class is used. It provides method for creating different types of AST nodes and immediate corresponding code formatting capabilities. Notice that for formatting it uses ‘$0’ placeholder instead of usual to .NET developers ‘{0}’.Element formatting works with ability to pass other PSI tree nodes.
"Did you mean?" QuickFix.
I will not dive deep into implementation details as I’m quite sure that you have got enough information already. This quick fix works with NotResolvedError highlighting and allows selecting type members that have most similar name to not resolved reference. As it was needed to provide multiple menu items implemented quick fix inherited directly from IQuickFix interface. Implementation uses Levenshtein distance to get most similar names and a part of ReSharper auto-completion API to get available symbols for specified reference.
Debugging and testing.
If you have created plugin project from available template it will start another Visual Studio instance in debug mode without any actions required. Basically the project has set “Start Action” set to “Start External Program” and command line arguments “/ReSharper.Plugin ReReflection.dll /ReSharper.Internal”. Specified command line arguments makes ReSharper to load created .dll as a plugin. “/ReSharper.Internal” switch enables access to many internal menus that helps to debug your plugin, analyze PSI tree, etc.
It is hard to make your plugin stable without continuous integration. Luckily ReSharper provides base classes for most kinds of extension points available. For reference you may see tests that were implemented for described quick fixes.
Conclusion.
The first article in the series describes basics of ReSharper plugin creation. Of course the code is far from production quality and during debug mode you may encounter unhandled exceptions reported by ReSharper. In the next article I will describe how custom highlighting are implemented and what ElementProblemAnalyzer is.
The code of plugin is available on GitHub.
Useful links.
- http://blogs.jetbrains.com/dotnet/tag/plugins/ - Official JetBrains blog
- http://hadihariri.com/2010/01/12/writing-plug-ins-for-resharper-part-1-of-undefined/
- http://mediocresoft.com/things/what-i-learned-about-writing-resharper-plugins
- https://code.google.com/p/agentjohnsonplugin/source/browse/#svn%2Ftrunk%2FAgentJohnson%2FCodeCleanup%2FRules – open source plugin
Very cool plugin! Have you packaged it up as an extension for the extension manager? http://confluence.jetbrains.com/display/NETCOM/1.9+Packaging+%28R8%29
ReplyDeleteThank you! For sure I will publish it to extensions gallery after several minor updates! Glad you liked it ;)
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDelete