10/31/2013

Writing a ReSharper plugin. Quick fixes.

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.

public interface IQuickFix
{
    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

[QuickFix]
public class SampleQuickFix : QuickFixBase
{
        public SampleQuickFix(AccessRightsError error)
        {
        }

        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.



10/24/2013

Localization of e-learnings - Articulate Studio in focus


With the rapid development of digital content and electronic media, more and more e-learning materials are coming our way for localization. Some were particularly tricky for getting them localized properly. In this article we will focus on e-learning courses created in Articulate Studio.

For those who have never heard of this product, it’s a solution for PowerPoint enhancement and design of e-learnings on its basis. This tool is fit for creating short e-learnings, especially if you already have a prepared PowerPoint presentation.  

Let us assume that you made a presentation for your co-workers on a new reporting approach in your company. Isn’t it reasonable to reinforce the presented information by using applicable training materials? You may want to consider Articulate Studio as a tool to help you.

With this software you will be able to fill your presentation with interactive quizzes, flowcharts and animated explanations. However, its primary benefit consists in providing an interactive course with presentation material, extended questions, audio feedback and additional reading. As a matter of fact a common PowerPoint presentation turns into a SCORM-compatible course with all its advantages.

Yet, at the same time, one might experience certain difficulties when localizing this type of material. Let’s suppose your company has other offices overseas, therefore it would be great to have these types of materials adapted for colleagues in other countries.

The first and biggest problem we have encountered concerns the process of translating the text content. Articulate Studio is equipped with a convenient method of exporting texts to .doc file. Unfortunately, there is no reverse conversion function. Without a reverse conversion you will have to substitute the original text with the translations manually, which takes lots of time and efforts. PowerPoint has the same flaw, though.

Apart from this not all interface strings of the e-learning are exported to .doc file, such help texts as “Click next to continue”, “Your score”, “Result”, etc often do not get to the .doc file.
Another disappointing moment consists in the limited number of built-in interface languages (only 10), though the product allows to add new languages manually.
It was noticed that the title length of the Articulate Quiz or Articulate Engage has limitations. When translating the English title we had to considerably shorten the translation.

However, on the whole if putting aside the localization problems this software solution can be considered sufficiently smart and helpful. The author has a possibility to add interactive content, flash videos, various quizzes, sound effects, and additional learning materials in the form of supplements, etc. The product is perfect for companies that have a large stock of learning materials, such as presentations, and wish to import them to LMS.

Just yesterday I got a new Articulate Studio version. One of the new features is import in Excel spreadsheet or txt file. I will write about the improvements soon. I hope there is something for localization too.

About Articulate Studio - http://www.articulate.com/

By Bohdan Kruk

Senior Localization Specialist