Monday, July 23, 2007

Creating Java refactorings with Scala and Eclipse LTK - Part 1

This is the first of a two-part series of posts on creating a simple Java refactoring using the Scala programming language and the Eclipse Language Toolkit.

The first part is about adding the required elements for the refactoring to appear in the Eclipse UI, the second part (which I think is the most interesting) is about manipulating the Java AST to actually perform the refactoring.

The purpose of this post is to show that Scala can be used to work with existing/complex Java APIs . The purpose of this post is not to how to create Eclipse refactorings, there's already nice documentation that does that.

The refactoring that will be implemented is called "Invert IF statement blocks", the purpose of this refactoring is to swap the THEN and ELSE sections of an IF statement preserving its behavior.

For example applying this refactoring to the following code:


if (x == 20) {
System.out.println("x is 20!");
} else {
System.out.println("x is NOT 20!");
}


Will result in the following code:


if (!(x == 20)) {
System.out.println("x is NOT 20!");
} else {
System.out.println("x is 20!");
}


Also, as an option, the refactoring is going to propagate the negation of the IF's condition expression. For example for code above the resulting statement will be:


if (x != 20) {
System.out.println("x is NOT 20!");
} else {
System.out.println("x is 20!");
}


In order to add the refactoring to Eclipse we're going to use the Eclipse Language Toolkit which makes it easy to access existing Eclipse infrastructure elements such as refactoring wizards, diff window, etc. A very nice explanation on how to create a refactoring using the Language Toolkit is found in Unleashing the Power of Refactoring, the code in this post is based on this article .

The first step for creating the refactoring, was to create an Eclipse Plugin . The first challenge was that using File -> New Project -> Plugin project will create a Java project will the required elements to create the plugin. But what we need is to create a Scala project with the same elements.

After trying several unsuccessful attempts, I found a couple of very useful posts from Neil’s point-free blog the first An OSGi Bundle… built in Scala and also Eclipse PDE does Scala . With this information I was able to create an Eclipse Plugin project with the Scala nature.

Based on Unleashing the Power of Refactoring, the minimal code required for a refactoring includes:


  • An action that is used to add the refactoring the UI

  • A refactoring wizard page for the refactoring arguments

  • A class that inherits from Refactoring that represents the refactoring and contains the AST manipulation code.


The action used for this post is associated with the menubar ( for future posts, I think the most appropriate location of this refactoring is on the Refactor menu of Eclipse). In order to add this to the UI the following information was added to the plugin.xml file:


<extension
point="org.eclipse.ui.actionSets">
<actionSet
id="ScalaTestRefactoring.actionSet1"
label="label">
<action
id="ScalaTestRefactoring.action1"
label="label">
</action>
</actionSet>
</extension>
<extension
point="org.eclipse.ui.actionSets">
<actionSet
description="Experimental actions"
id="org.eclipse.refactoring.actionSet"
label="Langexplr Section"
visible="true">
<menu
id="ScalaTestRefactoring.menu1"
label="Langexplr entries 2"
path="edit">
<separator
name="ScalaTestRefactoring.separator1">
</separator>
</menu>
<action
class="langexplr.eclipse.InvertIfStatementRefactoringAction"
definitionId="ScalaTestRefactoring.command1"
enablesFor="*"
id="ScalaTestRefactoring.action1"
label="Invert if statement(s)..."
menubarPath="ScalaTestRefactoring.menu1/ScalaTestRefactoring.separator1"
style="push">
</action>
</actionSet>
</extension>


Which says that the option "Invert if statement(s)..." will be added to a menu called "Langexplr entries 2" and that the action is implemented in the class langexplr.eclipse.InvertIfStatementRefactoringAction. More information about adding options to the Eclipse UI can be found in Contributing Actions to the Eclipse Workbench.

The code for the langexplr.eclipse.InvertIfStatementRefactoringAction class is the following:


class InvertIfStatementRefactoringAction extends IWorkbenchWindowActionDelegate {
var window : IWorkbenchWindow = null
var selection : ITextSelection = null
var compilationUnit : ICompilationUnit = null

override def dispose : unit = {
return
}
override def init( window : IWorkbenchWindow) = {
this.window = window;
}
override def run( action : IAction) = {
if (selection != null && window != null && compilationUnit != null) {
val refactoring = new InvertIfStatementRefactoring
refactoring.selection = selection
refactoring.compilationUnit = compilationUnit
val wizardOperation : RefactoringWizardOpenOperation =
new RefactoringWizardOpenOperation(
new InvertIfStatementRefactoringWizard(refactoring))
wizardOperation.run(window.getShell(), "Invert if statement");
}
}
override def selectionChanged( action : IAction, selection : ISelection) = {
this.selection = null;

if(selection.isInstanceOf[ITextSelection]) {
val s = selection.asInstanceOf[ITextSelection];
val cu = compilationUnitForCurrentEditor();
if (cu != null) {
compilationUnit = cu;
val selectionText = s.getText()
if (selectionText.indexOf("if") != -1) {
action.setEnabled(true)
this.selection = s
} else {
action.setEnabled(false)
}
}
} else {
action.setEnabled(false);
}
}
...
}


What this code actually does, is that it activates the action when the selected text contains the "if" string (which I know is a pretty weak criteria) more verifications are added later. Also when executed the action will run the wizard.

The refactoring wizard is only used to ask the user if the negation of the resulting IF statements needs to be propagated. In order to do this a wizard page most be created to ask that question. Here's the code for this page:


class InvertIfStatementRefactoringInputPage(name : String)
extends UserInputWizardPage(name) {
override def createControl(c : Composite) = {
val refactoringControl = new Composite(c,SWT.NONE)

this.setControl(refactoringControl)

val layout = new GridLayout
layout.marginTop = 10
layout.marginLeft = 10
layout.numColumns = 2

refactoringControl.setLayout(layout)

val iLabel = new Label(refactoringControl,SWT.NONE)
iLabel.setText("Propagate negation")
val cbButton = new Button(refactoringControl,SWT.CHECK)

cbButton.addSelectionListener(
new SelectionAdapter() {
override def widgetSelected(e : SelectionEvent) = {
val r = getRefactoring().asInstanceOf[InvertIfStatementRefactoring]
r.propagateNegation = cbButton.getSelection()
}
}
)
setPageComplete(true);
}
}


Given this we can create the class that represents the wizard:


class InvertIfStatementRefactoringWizard(r : Refactoring)
extends RefactoringWizard(r,
RefactoringWizard.DIALOG_BASED_USER_INTERFACE |
RefactoringWizard.PREVIEW_EXPAND_FIRST_NODE) {

override def addUserInputPages() = {
setDefaultPageTitle("Invert IF statement")
addPage(new InvertIfStatementRefactoringInputPage("InputPage"))
}
}


The resulting dialog looks like this:



In the second part, the implementation of the actual refactoring will be presented.

Code for this experiment can be found here.