jdt ui R3.2.x
java development tooling ui

New Rename Type Feature: Update Similarly Named Elements

This document is intended for providers of participants for the rename type, method, and field refactorings. The document describes the new rename type feature "Update Similarly Named Elements" and the changes required in rename type, method, and field refactoring participants induced by this new feature.

Table of Contents:

I. Introduction
II. Signalling the new changes
III. Required actions
IV. How to write similarly named declarations-aware participants (migration guide)

Introduction

Starting in Eclipse 3.2 M4, the rename type refactoring is able to rename “similar declarations": methods, fields, and local variables with names similar to the name of the renamed type. For example, consider the following code:
public class Foo {
    private Foo foo;
    private Foo() {
    }
    static Foo createFoo() {
        return new Foo();
    }
    public Foo getFoo() {
        return foo;
    }
    public void setFoo(Foo foo) {
      this.foo= foo;
    }
}
Renaming the type Foo to Bar used to update only the type’s name and all references to it, for example the return type of createFoo() or the parameter type of setFoo(Foo foo). The new update similar declarations feature allows the user to change all method, field, and variable names corresponding to or containing the type name. For example, when renaming Foo to Bar, the code may be changed in the following way (depending on the users choices):
public class Bar {
    private Bar bar;
    private Bar() {
    }
    static Bar createBar() {
        return new Bar();
    }
    public Bar getBar() {
        return bar;
    }
    public void setBar(Bar bar) {         this.bar= bar;     } }
In contrast to the old type rename refactoring whose changes were limited to the type itself and its references, the new rename type refactoring creates much more changes which also affect arbitrary fields, methods, and local variables inside and outside of the renamed type. This raises the question of how to signal those changes to interested parties by loading corresponding participants.
Signalling the new changes

The best solution would have been to simply reuse existing participants, i.e. loading
  • a rename type participant for the type rename,
  • rename method participants for the method renames,
  • rename field participants for the field renames, and
  • rename local variable participants for the local variable renames.
Unfortunately, this approach was not feasible as the changes created by the new rename type refactoring are no longer simple renames – neither the type rename nor the method, field and local variable renames:

Type rename: Considering the example above, the normal rename type refactoring would only have changed the type name and the references to the type; for example, the type of the field foo and the return type of method getBar(). Rename type participants could therefore assume that all fields and methods inside the type would keep their defining attributes:
  • same name for methods, fields, and local variables,  and
  • same parameter types for methods
In the new refactoring, this is now no longer the case: Each field, method, or local variable inside the renamed type is potentially affected by the refactoring. Consider the following method from the example above:
public void setFoo(Foo foo) { }
Renaming the type Foo to Bar effectively changes this method to
public void setBar(Bar bar) { }
The changes to this method are:
  1. the name changed
  2. the parameter type changed
  3. the parameter name changed
These changes are best described as a complete signature change of the method, and not a simple rename. Similar problems exist when changing fields and local variables. Because these changes were not signaled to the participants, they break existing participants with assumptions about these members. Those participants cannot be reused as-is.

As a side note, signature changes to methods have already been a problem when renaming methods like the method setFoo(Foo foo): Even in the former implementation, a signature change was performed on this method as the parameter type Foo was changed to Bar which was not signaled to the rename type participant; we also did not load a special participant for it.

Method, field and local variable renames: Considering a rename of the method setFoo(Foo foo) from the example above, the normal rename method refactoring would only have changed the name of the method (and possibly of overridden methods and references). Rename method participants could therefore assume that only the name of the method changed and everything else stayed the same.

In the new refactoring, this is now no longer the case. Consider the method signature change of setFoo(Foo foo) from the example above. The changes to this method are:
  1. the name changed
  2. the parameter type changed
  3. the parameter name changed
  4. the container of the method changed: it is now no longer declared in the class Foo, but in the class Bar.
Again, these changes are best described as a complete signature change of the method and an additional change to the container of the method; it is no longer a simple rename. Because these changes were not signaled to the participants, they break existing participants. Therefore, existing rename method, field, and local variable participants cannot be reused as-is, and it is questionable whether those participants should know about possible container changes.
Required Actions

As outlined in section II, some existing participants will not work with the new refactoring. Because of the breaking nature of this change, we provide
  • a product configuration flag to completely disable the “update similar names” feature
  • a participant descriptor flag to temporarily disable participants if the “update similar names” feature is turned on
  • a migration guide and helper methods for implementing support for the new feature.
Setting the product configuration flag: If set, the new feature will not be presented to the user at all and participant loading and handling stays exactly the way it was before. Note that the feature is enabled by default. To disable the feature, please add the following code to the product extension definition:
<property 
   name="org.eclipse.jdt.ui.refactoring.handlesSimilarDeclarations" 
   value="false"
/>
Setting the participant descriptor flag: If set, the participant will be disabled in each concrete rename type refactoring if the new option is enabled. To disable a participant in that case, please add the following code to the participant extension definition:
<param name="handlesSimilarDeclarations" value="false"/>
Updating the participants (Migrating): To be able to respond to the additional changes of the new type rename refactoring, participants will need to know about these changes. Our solution for this problem is new API for the rename type participants, enabling them to handle similarly named declarations. Participant writers will need to adapt or replace their existing rename type participants to
  • be aware of changing fields, methods, or local variables inside the renamed type (signature change and container change)
  • be aware of changing fields, methods, or local variables outside the renamed type (signature change, possibly container change if local types are used)
Rename method, field, and local variable participants will not be loaded for similarly named declarations; these elements must be handled inside the rename type participant.
Migration Guide

Not all existing rename type participants will need code changes, and the amount of changes will depend significantly on the existing implementation. Particularly, rename type participants which only deal with the type itself and not its members or other members (for example, for updating a launch configuration) need not be changed at all.

For all participants which deal with the members of the type or which need to add support for method, field, or local variable renames, the code handling those elements must be changed. The biggest problem when writing the new participant is mapping the renamed old methods, fields, and local variables to the new, changed elements since a lot of things can change (the complete signature and the direct or indirect container of the elements).

To solve this problem, we provide API for relocating any java element or resource in the workspace to reflect all performed changes. Participants can use the relocated handle during change execution to access the new elements.

Let’s look at a simple example. Consider the following old rename type and rename method/field/local variable participants which keep a list of all keys of types, methods, fields, and local variables of all Java projects in an external database.

The rename participants are implemented something like this (this is the rename type participant, which we will adapt – the code of the method/field/local variable participants looks similar, but need not be changed):
public class DBTypeRenameParticipant extends RenameParticipant {
   private IType fOldType;
   private String fNewName;
   private IType fNewType;
   protected boolean initialize(Object element) {
       fOldType= (IType)element;
       fNewName= getArguments().getNewName();
       // (assuming top level types)
       fNewType= fOldType.getPackageFragment().getCompilationUnit (fNewName + ".java").getType(fNewName);
       return true;
   } 
   public Change createChange(IProgressMonitor pm) {
       return new DBChange(fOldType, fNewType) {
          public Change perform(…) {
             getDB().update(fOldType.getKey(), fNewType.getKey());
             for (all fields of old Type)
                getDB().update(oldField.getKey(), fNewType.getField(
			oldField.getElementName().getKey());
            }
            // methods, local variables…
        };
    }
}
The code makes the assumption that all fields in the new type have the same name as in the old type. We need to:
  • change this assumption
  • add code for handling field, method and local variable changes in other types
Let’s first look at how we can find out about similarly named declarations. As a subclass of RenameParticipant, we have access to the RenameArguments, which we already use in initialize(Object element):
fNewName= getArguments().getNewName();
The RenameArguments class has been subclassed for the Rename Type Refactoring to contain additional information about similarly named declarations. We can cast the class to RenameTypeArguments and check if similarly named declarations are renamed:
RenameTypeArguments args= (RenameTypeArguments) getArguments();
if (args.getUpdateSimilarDeclarations()) {
    // do something
}
Now we’d like to know which elements are renamed. Again, we ask the RenameTypeArguments for this information:
IJavaElement[] similarDeclarations= args.getSimilarDeclarations();
All of the elements in this array are either of type IMethod, IField, or ILocalVariable. These are the handles of the “old” elements before all changes.

Now we know which elements have changed, but we still don’t know how they will look after the refactoring. What we’d like to do is change our existing code which resolves the new elements to handling complete signature changes, i.e. we’d like to change the lines
fNewType= fType.getPackageFragment().getCompilationUnit (fNewName + ".java").getType(fNewName);
and
fNewType.getField(oldField.getElementName().getKey());
To do this, we ask our refactoring processor for the new elements. The processor is guaranteed to implement the interface IJavaElementMapper – a mapper to map old pre-refactoring handles to post-refactoring handles. We adapt the processor to the interface:
IJavaElementMapper processor = (IJavaElementMapper) getProcessor().adapt(IJavaElementMapper.class);
Now, we can call the following method on the mapper:
IJavaElement getRefactoredJavaElement(IJavaElement element);
We can use this method to ask the refactoring to relocate any java element in the workspace to reflect all performed changes.  For example, when asking the refactoring for an updated handle of the method:
“setSomeClass(SomeClass someClass) in InnerType in SomeClass”, 
It will return the handle:
“setAnotherClass(AnotherClass anotherClass) in InnerType in AnotherClass” 
(Provided the class SomeClass was renamed to AnotherClass).

During change creation, the new handle will still point to a non-existing element. However, during participant change execution, the element will exist and can be used to retrieve resources etc.

Side Note: The Rename Type Refactoring may also affect resources: If a top-level type was renamed, its enclosing file will be renamed as well. To be able to re-map such resource elements as well, we can adapt the RenameTypeProcessor to the IResourceMapper interface:
IResourceMapper processor = (IResourceMapper) getProcessor().adapt(IResourceMapper.class);
Now, we can call the following method on the mapper:
IResource getRefactoredResource(IResource element);
Again, this method can be used to map any IResource in the workspace to a new element reflecting all changes.

With these helper methods, we change our code to the following to respond to signature changes in members of the type itself and also to changes to fields, methods, and local variables outside of the type:
public class DBTypeRenameParticipant extends RenameParticipant {
   private IType fType;
   private IJavaElementMapper fMapper;
   private IJavaElement[] fSimilarDeclarations;
   protected boolean initialize(Object element) {
      fType= (IType)element;
      RenameTypeArguments args= (RenameTypeArguments) getArguments();
      if (args.getUpdateSimilarDeclarations()) {
         fSimilarDeclarations= args. getSimilarDeclarations();
      }
      fMapper= (IJavaElementMapper) getProcessor().adapt(IJavaElementMapper.class);
      return true;
}
public Change createChange(IProgressMonitor pm) {
    return new DBChange(fType, fNewType) {
        public Change perform(…) {
            getDatabase().update(fType.getKey(), fMapper.getRefactoredElement(fType).getKey());
            for (all members of old Type)
                getDB().update(oldMember.getKey(),fMapper.getRefactoredElement(oldMember).getKey();
            }
            for (all similar declarations not in old type) {    
getDB().update(oldElement.getKey(), fMapper.getRefactoredElement(oldElement).getKey()); }         };     } }
Observe the following changes:
  • We acquired an instance of the processor which we used to resolve refactored elements
  • We acquired a list of similarly named declarations
  • We used the java element mapper to acquire the type after the change
  • We used the java element mapper to acquire the type’s members after the change. Note that the resolving method works for all members of the type, not only the similarly named ones. In fact, the method works for any IJavaElement in the whole workspace (of course, most will remain unaffected).
  • We used the similarly named declarations list and the java element mapper to change all similarly named declarations outside the type.
  • We didn’t use a resource element mapper in this example; however it works similar to the java element mapper.
Our code is now fully adapted to the similarly named declaration updating feature.

To sum up, you need to be aware of the following issues when adapting your old rename type participant code:
  1. You may no longer make assumptions about how a java element or resource looks like during change execution, whether it resides inside the renamed type or outside. Instead, always use the getRefactoredJavaElement() or getRefactoredResource() methods to acquire a new instance of the java element or resource.
  2. You need to take care of methods, fields, and local variables renamed outside the type. You may acquire them by using the getSimilarDeclarations() method.