S-JIS[2018-09-16/2018-09-22] 変更履歴

Eclipse JDT リファクタリング

Eclipseプラグイン開発JDTでのリファクタリングについて。

 

概要

Javaエディターでリファクタリングを行う事が出来る。

なお、リファクタリングのメニューIDは「org.eclipse.jdt.ui.refactoring.menu」だが、これにメニューを追加しようとしても出来ない。(バグ?)


改名の例

クラス名やメソッド名などを改名する場合はRenameSupportというクラスが利用できる。

プラグインとしては「org.eclipse.jdt.ui」に属していると思うが、それだけだと、org.eclipse.jdt.core.refactoring.descriptors.RenameJavaElementDescriptorが見つからなくてコンパイルエラーになった。 (Eclipse 4.4.2)
plugin.xmlのDependenciesのImport Packagesに「org.eclipse.jdt.core.refactoring.descriptors」を追加したら大丈夫になった。


下記のMyRenameHandlerは、独自メニューを追加しておき、そのメニューから呼ばれる。
このとき、改名したいクラス名やメソッド名が(Javaエディター上で)選択された状態になっている想定。

MyRenameHandler.java:

import java.lang.reflect.InvocationTargetException;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.ui.refactoring.RenameSupport;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.texteditor.ITextEditor;
public class MyRenameHandler extends AbstractHandler {

	// @Override
	public Object execute(ExecutionEvent event) throws ExecutionException {
		IEditorPart editor = HandlerUtil.getActiveEditor(event);
		if (editor instanceof ITextEditor) {
			IJavaElement element = getJavaElement((ITextEditor) editor);
			if (element instanceof IMethod) {
				String newName = "newMethodName"; // 本来はウィザードか何かで新しい名前を入力してもらう
				rename(editor.getSite(), (IMethod) element, newName);
			}
		}

		return null;
	}
	private IJavaElement getJavaElement(ITextEditor editor) {
		IEditorInput input = editor.getEditorInput();
		IJavaElement element = (IJavaElement) input.getAdapter(IJavaElement.class);
		if (element == null) {
			return null;
		}

		try {
			ITypeRoot root = (ITypeRoot) element.getAdapter(ITypeRoot.class);

			ITextSelection selection = (ITextSelection) editor.getSelectionProvider().getSelection();
			int offset = selection.getOffset();

			return root.getElementAt(offset);
		} catch (JavaModelException e) {
			return element;
		}
	}
	private void rename(IWorkbenchPartSite site, IMethod method, String newName) {
		try {
			RenameSupport support = RenameSupport.create(method, newName, RenameSupport.UPDATE_REFERENCES);

			Shell shell = site.getShell();
			// support.openDialog(shell);

			IRunnableContext context = site.getWorkbenchWindow();
			support.perform(shell, context);
		} catch (CoreException e) {
			〜
		} catch (InvocationTargetException e) {
			〜
		} catch (InterruptedException e) {
			〜
		}
	}
}

RenameSupport.createメソッドでRenameSupportインスタンスを生成する。
ITypeやIMethod等に応じて異なるcreateメソッドが用意されている(オーバーロードされている)。

RenameSupport#openDialogメソッドを呼ぶと、ウィザードが開く。
このウィザードでプレビューを見たりすることが出来る。
ウィザードでFinishボタンを押すと改名が実行される。

RenameSupport#performメソッドを呼ぶと、(ウィザード等のダイアログを開かずに)改名が実行される。


RenameParticipantの例

RenameParticipantというエクステンションを使うと、通常のリファクタリングの改名の動作に対して独自の処理を追加することが出来る。

下記は、メソッド改名時に、そのメソッドと同じソースファイルの文字列内にメソッド名と同名の文字列があったら新しい名前に変更する例。

plugin.xml:

	<extension
		point="org.eclipse.ltk.core.refactoring.renameParticipants">
		<renameParticipant
		    id="jp.hishidama.example.myRenameParticipant"
		    name="My Rename"
		    class="jp.hishidama.example.MyRenameParticipant">
			<enablement>
			</enablement>
		</renameParticipant>
	</extension>

MyRenameParticipant.java

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.refactoring.CompilationUnitChange;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor;
import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
// http://help.eclipse.org/kepler/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Freference%2Fextension-points%2Forg_eclipse_ltk_core_refactoring_renameParticipants.html
// http://www.eclipse.org/jdt/ui/r3_2/RenameType.html
public class MyRenameParticipant extends RenameParticipant {

	@Override
	public String getName() {
		return "My Rename";
	}
	@Override
	protected boolean initialize(Object element) {
		if (!getArguments().getUpdateReferences()) {
			// リファクタリングのダイアログで、連動して更新しないよう指定された場合
			return false;
		}


		if (element instanceof IMethod) {
			return true;
		}

		return false;
	}

initializeメソッドに変更対象オブジェクトが入ってくるので、自分が処理する対象かどうかを返す。

変更対象オブジェクトは複数存在する(MyRenameParticipantが複数回呼ばれる)ことがある。[2018-09-22]

ユーザーが変更したもの initializeに渡ってくるオブジェクト 備考
メソッド名 IMethod  
クラス名
クラスのソースファイル
IType(, ICompilationUnit, IFile) クラスがファイル名と同名のクラスの場合、ICompilationUnit, IFileも呼ばれる。
パッケージ名 IPackageFragment サブパッケージも連動して変更するように指定されていると、サブパッケージの個数分呼ばれる。

複数回呼ばれる際は、MyRenameParticipantは呼ばれる度に別のインスタンスになっているので、フィールドに値を保持しても大丈夫なようだ。

	@Override
	public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) throws OperationCanceledException {
		return null;
	}
	@Override
	public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException {
		Change result = null;
		CompositeChange compositeChange = null;

		RefactoringProcessor processor = /*super.*/getProcessor();
		Object[] elementList = processor.getElements();
		SubMonitor subMonitor = SubMonitor.convert(pm, elementList.length);

		for (Object element : elementList) {
			Change change = null;
			if (element instanceof IMethod) {
				IMethod method = (IMethod) element;
				change = createChange(method);
			}

			if (change != null) {
				if (result == null) {
					result = change;
				} else {
					if (compositeChange == null) {
						compositeChange = new CompositeChange(getName());
						compositeChange.add(result);
						result = compositeChange;
					}
					compositeChange.add(change);
				}
			}

			subMonitor.worked(1);
		}

		return result;
	}

	private Change createChange(IMethod method) {
		ICompilationUnit cu = method.getCompilationUnit();
		String oldName = method.getElementName();
		String newName = /*super.*/getArguments().getNewName();
		StringFinder finder = new StringFinder(cu, oldName, newName);
		List<ReplaceEdit> list = finder.getEditList();
		if (list.isEmpty()) {
			return null;
		}

		Change result;

		TextChange change = /*super.*/getTextChange(cu); // 他のRenameParticipantで更新済みのTextChangeを取得する
		if (change == null) {
			change = new CompilationUnitChange(cu.getElementName(), cu);
			change.setEdit(new MultiTextEdit());
			result = change;
		} else {
			// 他のRenameParticipantで既に更新済みの場合は、戻り値はnullにしないと駄目な模様(Eclipse 4.4.2)
			// メソッド名の変更なら、同ソースファイルは更新済み
			result = null;
		}

		for (ReplaceEdit edit : list) {
			change.addEdit(edit);
		}

		return result;
	}

createChangeメソッドで変更箇所を返す。

getProcessor().getElements()で変更対象のオブジェクト(この例ではIMethod)が取得できる。
戻り値は配列だが、メソッド名の改名の場合は要素数は1個。

この例ではソースファイル内の文字列を変更するので、ASTVisitorでソースファイル内の変更対象文字列の位置を収集している。
ReplaceEditが変更元の位置と変更後の文字列を保持する。
をれをTextChangeに追加していく。(TextChangeは変更対象ソース毎に別々のインスタンス)
(ReplaceEditは変更前の位置でインスタンスを作る。同一ソースファイル内で複数個所の変更をする場合、置換後の文字列の長さが変わっても、変更位置は自動的に追随される)

	private static class StringFinder extends ASTVisitor {
		private final ICompilationUnit unit;
		private final String oldName;
		private final String newName;

		private List<ReplaceEdit> editList = new ArrayList<ReplaceEdit>();

		public StringFinder(ICompilationUnit unit, String oldName, String newName) {
			this.unit = unit;
			this.oldName = oldName;
			this.newName = newName;
		}

		public List<ReplaceEdit> getEditList() {
			visit();
			return editList;
		}

		private boolean visited = false;

		private void visit() {
			if (visited) {
				return;
			}
			visited = true;

			ASTParser parser = ASTParser.newParser(AST.JLS8);
			parser.setSource(unit);
			ASTNode node = parser.createAST(new NullProgressMonitor());
			node.accept(this);
		}

		@Override
		public boolean visit(StringLiteral node) {
			String value = node.getLiteralValue().trim();
			if (value.equals(oldName)) {
				int offset = node.getStartPosition() + 1;
				int length = node.getLength() - 2;
				ReplaceEdit edit = new ReplaceEdit(offset, length, newName);
				editList.add(edit);
			}
			return false;
		}
	}
}

MoveParticipantの例

MoveParticipantというエクステンションを使うと、通常のリファクタリングの移動の動作に対して独自の処理を追加することが出来る。[2018-09-22]

plugin.xml:

	<extension
		point="org.eclipse.ltk.core.refactoring.moveParticipants">
		<renameParticipant
		    id="jp.hishidama.example.myMoveParticipant"
		    name="My Rename"
		    class="jp.hishidama.example.MyMoveParticipant">
			<enablement>
			</enablement>
		</renameParticipant>
	</extension>

MyMoveParticipant.java

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.IType;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
import org.eclipse.ltk.core.refactoring.participants.MoveParticipant;
public class MyMoveParticipant extends MoveParticipant {

	@Override
	public String getName() {
		return "My Move";
	}
	@Override
	protected boolean initialize(Object element) {
		if (!getArguments().getUpdateReferences()) {
			return false;
		}

		if (element instanceof IType) {
			return true;
		}

		return false;
	}
	@Override
	public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) throws OperationCanceledException {
		return null;
	}
	@Override
	public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException {
		// 移動先
		Object destination = getArguments().getDestination();
		if (destination instanceof IPackageFragment) {
			IPackageFragment fragment = (IPackageFragment) destination;
			String newPackageName = fragment.getElementName();
			〜
		}

		〜

		return change;
	}

createChangeの中身はRenameParticipantと同様。

移動先はgetArguments().getDestination()で取得できる。
クラス(IType)の移動の場合、要するにパッケージの変更なので、移動先のオブジェクトとしてはIPackageFragment(移動先の新しいパッケージ)が返ってくる。


JDTへ戻る / Eclipseプラグインへ戻る / Eclipseへ戻る / 技術メモへ戻る
メールの送信先:ひしだま