Refactoring Developer Guide
Author:
Martin Matula,
Jan Becicka
This document provides some basic information that will help you to get started developing experimental refactorings.
It is not a definitive guide. Besides this guide, you can get more information about implementing a refactoring by looking
at the existing refactorings. We implemented Pull Up refactoring as the reference refactoring you can learn from.
- Contents:
- Getting The Sources
- Coding Tips
- Implementing Non-Visual Part
- Refactoring Class
- Refactoring Plugin Class
- Internationalization
- Unit Tests
- Implementing User Interface
- Refactoring Action
- RefactoringUI Class
- Parameters Panel
- Accessibility
- Useful Links
All the new refactorings should be developed in refactoring/experimental module. You should start developing by checking
out all NetBeans sources from the CVS (e.g. by checking out standard_nowww module) and opening the refactoring/experimental
module (located in $cvsroot/refactoring/experimental directory) and all projects it depends on.
Since we would like to be able to maintain your code, please use code comments where appropriate and try to follow the
recommended coding conventions. All methods should have Javadoc.
If you get stuck on any problem during the implementation or you have some doubts, do not hesistate to ask on the
dev@refactoring mailing list. We will be happy to help you. And we will
try to do that as soon as possible.
During the implementation you will likely find out that some things are missing in the spec., or that the spec. should be
modified in some other way. Please keep the refactoring specification in sync with your implementation by sending us
updates.
The non-visual part of the refactoring implements the refactoring logic. It consists basically of two classes - "refactoring"
class and "refactoring plugin" class.
The refactoring class serves as an API for invoking the refactoring. Also it is used by various refactoring plugins
to determine refactoring parameters. This class itself should do almost no work - all the work is done by the plugins. The refactoring
class usually contains just getters and setters for the refactoring parameters.
The refactoring class should be placed in org.netbeans.modules.refactoring.experimental.
Its name should be RefactNameRefactoring - so for example for Pull Up refactoring it is
PullUpRefactoring. The class needs to extend AbstractRefactoring (from the Refactoring API)
and it must be declared as final (since eventually it will become an API).
Constructor of the class usually takes the element the refactoring can be invoked on. This element can then be used
by refactoring plugins to check pre-conditions.
Besides the standard getters and setters for the parameters of the refactoring, the class may also contain additional
public helper methods and final inner classes. Inner classes are used mostly as helper structures to represent refactoring
parameters (see PullUpRefactoring.MemberInfo). Helper methods are useful for providing information that could
be useful to various plugins and refactoring clients especially if it makes sense to provide this information from one central point
for the purpose of caching (see PullUpRefactoring.collectSupertypes()).
Every refactoring has plugins - it should have at least one plugin - these actually do all the work. The refactoring module
itself should provide the basic plugin that does the J2SE refactoring. Other modules can add other plugins - this makes
them able to participate in the refactoring. For example, when renaming a method, a J2EE plugin needs to rename the related
methods in other interfaces of an EJB (if necessary) and change the deployment descriptor. The plugin class that we will
discuss here is the basic plugin class that does all the basic J2SE work.
The refactoring plugin class should be placed in org.netbeans.modules.refactoring.experimental.plugin
package and named RefactNameRefactoringPlugin (e.g. PullUpRefactoringPlugin).
The plugin should extend JavaRefactoringPlugin class defined in refactoring module - the class implements
RefactoringPlugin interface which every refactoring plugin has to implement and it adds some helper methods that
your implementation may find useful.
Plugins are instantiated by the refactoring class automatically when some code creates an instance of a refactoring. The instantiation
of the plugins is done by plugin factories (registered in the NetBeans lookup via META-INF/services folder in the module jar) that
get called by the refactoring. Our refactoring/experimental module has also such plugin factory - see
PluginFactory class in org.netbeans.modules.refactoring.experimental.plugins package.
To make sure your refactoring plugin gets created when a given refactoring is invoked, you need to add the following
instantiation code for your plugin at the beginning of the createInstance() method of the factory:
if (refactoring instanceof YourRefactoring) {
return new YourRefactoringPlugin((YourRefactoring) refactoring);
}
As you can see, the createInstance() method takes the parent refactoring as the parameter. Every plugin should
keep a reference to the parent refactoring to be able to get refactoring parameters from it. That's why the plugins usually
take the refactoring as a constructor parameter (as in the case of the example code above, or PullUpRefactoringPlugin).
Now, there are four methods that need to be implemented by the plugin:
preCheck() - checks pre-conditions described in Pre-Check section of the refactoring specification document.
fastCheckParameters() - checks validity of parameters - does only the checks that can be performed
quickly and do not require a complex computation. Corresponds to Fast Parameters Check section of the refactoring specification.
checkParameters() - does all the other validity checks of parameters (not covered by
fastCheckParameters() method). Corresponds to Parameters Check section of the refactoring specification.
prepare() - responsible for creating descriptors of changes (instances of implementations of RefactoringElement)
that will be made by the refactoring. This method corresponds to the Changes To Be Made section of the refactoring specification.
Implementing these methods will require usage of the new Java Language API. Since this API is quite complex and it is
not well documented, please refer to the source code of existing refactorings to get some idea of how to do certain things.
Also check out FAQ page where we publish code snipets for achieving some common tasks and please
ask on dev@refactoring mailing list if you don't find solution to your
problem - it is much more effective than spending several hours of hopeless guessing and others will be able to also benefit from our
discussion.
Please note that all the above methods you need to implement return an instance of class named Problem. Instances
of this class represent problems that may be fatal or non-fatal for performing the refactoring. The methods are supposed to do
various checks (as outlined in the specification for a given refactoring) and return the problems they find as the instances
of the mentioned Problem class. Problems can be chained (using Problem.setNext() method), which makes
it possible to return several problems from a single operation. However there is a rule that fatal problems must come
first in the chain. Creating a problem using JavaRefactoringPlugin.createProblem() method will automatically
ensure this. Also, the methods except for fastCheckParameters() are expected to fire progress events, since they are performing
potentially time consuming operations. You can see how to do this in PullUpRefactoringPlugin.
The prepare() method is kind of special, so we will discuss its implementation in this separate sub-section.
This method is responsible for actually telling the refactoring how it should be performed. It does it by creating instances
of specialized implementations of RefactoringElement - each of the instances represents a single change that the refactoring
should do. So, e.g. in case of a Rename Field refactoring, every occurrence of an access to the field being renamed would
have a correcponding RefactoringElement that would represent renaming of that single occurrence. So, besides
implementing prepare() method itself, you will in fact also need to create the implementations of RefactoringElement
interface that will be used to perform the changes.
Refactoring elements are usually created as subclasses of SimpleRefactoringElementImpl. Here is a simple description
of the methods you will need to implement:
performChange() - performs the change represented by the refactoring element.
getText() - returns text for the refactoring element (usually contains the fragment of code that the refactoring
element will change, or a text describing what the refactoring element will do).
getDisplayText() - usually same as getText() but may contain HTML tags (e.g. if returning a line
containing the code that will be changed, the HTML tags can be used to display the very piece of code to be changed in bold).
getParentFile() - file that will be affected by the change represented by the refactoring element.
getJavaElement() - Java element this change relates to.
getPosition() - document position of the related code.
The refactoring elements are instantiated by the prepare() method and added to the instance of RefactoringElementsBag
that serves as an output parameter.
All message strings should be contained in a Bundle.properties file (containing default message strings - i.e. in US English).
This resource bundle is conventionally located in the same directory (package) as the source files which use it; other source packages should have their own bundles.
If you have a string literal in code which you know should never be localized, then end that line of source code with the following "end-of-line" comment: // NOI18N.
Generally each NetBeans developer should follow NetBeans Module Internationalization Guide.
Very important verification of quality of any piece of code is an automated test suite. Each refactoring should have its own set of tests.
Most of NetBeans modules uses our test harness called xtest which is based on JUnit
and enhances it with a few additional features (tests should inherit from NbTestCase)
and configuration framework.
Test scripts for refactorings are already prepared for you. The only thing you need to do is to write your own test.
The name of the test class should end with *Test postfix to be recognized as a unit test.
Please take a look at PullUpRefactoringTest.
It is located under refactoring/experimental/test/unit/src source root, where all tests should be placed.
As you can see writing tests is not difficult. Framework is prepared. You simply have to put your tests into refactoring/experimental/test/unit/src source root.
Files needed by your tests put into test project: refactoring/experimental/test/unit/data/projects/default/src/
and golden files put into refactoring/experimantal/unit/data/goldenfiles.
To run all experimental refactoring tests just do
cd refactoring/experimental/test
ant
The UI part of the refactoring typically consists of three components:
- Refactoring action - Presents the refactoring feature in a menu. Can be used to invoke the refactoring.
- RefactoringUI class - Implementation of
RefactoringUI interface. Plugs into the refactoring
framework. Provides refactoring parameters panel, display name of the refactoring, reference to the
Refactoring Class, etc.
- Refactoring parameters panel - Refactoring-specific JPanel that will be displayed in the generic refactoring wizard
to collect refactoring parameters.
All the above UI components of the refactoring should be placed into
org.netbeans.modules.refactoring.experimental.ui
package. You can find
PullUpAction,
PullUpRefactoringUI and
PullUpPanel in this package.
These classes are properly documented and serve as a model example. Reading their source code should significantly help you in
writing these components for your own refactorings.
To implement refactoring action, create a subclass of org.netbeans.modules.refactoring.spi.ui.AbstractRefactoringAction
and name it RefactNameAction. Constructor can be basically copied from PullUpAction - the
only thing you need to change is the bundle key you use to get the action's name from the bundle (make sure to add this bundle
key with a proper value to the Bundle.properties file). Same for iconResource() method.
The interesting part is in the enable() and createRefactoringUI() methods. The first method (enable())
determines when the action should be enabled based on the currently active (selected) nodes in the IDE. By convention
the implementation of this method should not do anything expensive - preferably it should not tough the Java metadata and
decide purely on whether there are JavaDataObjects behind the selected nodes and how many nodes are selected (some actions
may be applicable to several nodes at once as in case of Pull Up refactoring, where you can select several members to be
pulled up, some actions may be able operate on a single node only). For performance reasons the enable() method
does not get information about the position of the caret in the editor - that's why the checks in this method should be weak.
Most of the other checks should be done in refactoring preCheck() method (we talked about this method earlier),
which can provide user with a descriptive message for why the refactoring cannot be performed on a selected object and how
user can fix it.
The other method (createRefactoringUI) gets called when the action is invoked by user. The method receives active
nodes and also the element that represents the text under the cursor. Based on that the method should construct a set of elements
(or a single element) that the refactoring should be performed on and pass that into a new instance of refactoring UI object
that should be returned from this method.
TBD
Refactoring API
Copy Class Refactoring Module Tutorial
Experimental Refactoring Implementation & JMI - FAQ
NetBeans Wiki
Module Building: A Quick Tour of NetBeans Idioms and Infrastructure