ViewerSorterPlugin.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.AstProcessorBuilder;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.JfaceCleanUpFixCore;
/**
* Cleanup transformation for migrating from deprecated ViewerSorter to ViewerComparator.
*
* <p>This helper transforms ViewerSorter usage patterns in Eclipse JFace code:</p>
* <ul>
* <li>Converts {@code ViewerSorter} to {@code ViewerComparator}</li>
* <li>Converts {@code TreePathViewerSorter} to {@code TreePathViewerComparator}</li>
* <li>Converts {@code CommonViewerSorter} to {@code CommonViewerComparator}</li>
* <li>Converts {@code getSorter()} and {@code setSorter()} method calls to {@code getComparator()} and {@code setComparator()} respectively (for JFace viewers only)</li>
* </ul>
*
* <p><b>Migration Pattern:</b></p>
* <pre>
* // Before:
* public class MyViewer extends ViewerSorter {
* viewer.setSorter(new ViewerSorter());
* ViewerSorter sorter = viewer.getSorter();
* }
*
* // After:
* public class MyViewer extends ViewerComparator {
* viewer.setComparator(new ViewerComparator());
* ViewerComparator comparator = viewer.getComparator();
* }
* </pre>
*
* @see org.eclipse.jface.viewers.ViewerSorter
* @see org.eclipse.jface.viewers.ViewerComparator
*/
public class ViewerSorterPlugin extends
AbstractTool<ReferenceHolder<Integer, ViewerSorterPlugin.SorterHolder>> {
/** Deprecated ViewerSorter class */
private static final String VIEWER_SORTER = "org.eclipse.jface.viewers.ViewerSorter"; //$NON-NLS-1$
/** Replacement ViewerComparator class */
private static final String VIEWER_COMPARATOR = "org.eclipse.jface.viewers.ViewerComparator"; //$NON-NLS-1$
/** Deprecated TreePathViewerSorter class */
private static final String TREEPATH_VIEWER_SORTER = "org.eclipse.ui.navigator.TreePathViewerSorter"; //$NON-NLS-1$
/** Replacement TreePathViewerComparator class */
private static final String TREEPATH_VIEWER_COMPARATOR = "org.eclipse.jface.viewers.TreePathViewerComparator"; //$NON-NLS-1$
/** Deprecated CommonViewerSorter class */
private static final String COMMON_VIEWER_SORTER = "org.eclipse.ui.navigator.CommonViewerSorter"; //$NON-NLS-1$
/** Replacement CommonViewerComparator class */
private static final String COMMON_VIEWER_COMPARATOR = "org.eclipse.ui.navigator.CommonViewerComparator"; //$NON-NLS-1$
/**
* Holder for ViewerSorter-related transformation data.
* Tracks types and method names that need to be replaced.
*/
public static class SorterHolder {
/** Types that need to be replaced */
public Set<Type> typesToReplace = new HashSet<>();
/** Method names that need to be replaced (e.g. getSorter -> getComparator, setSorter -> setComparator) */
public Set<Name> methodNamesToReplace = new HashSet<>();
/** Nodes that have been processed to avoid duplicate transformations */
public Set<ASTNode> nodesprocessed;
}
/**
* Checks if a type is one of the deprecated ViewerSorter types.
*
* @param typeBinding the type binding to check
* @return {@code true} if it's a ViewerSorter type, {@code false} otherwise
*/
private static boolean isViewerSorterType(ITypeBinding typeBinding) {
if (typeBinding == null) {
return false;
}
String qualifiedName = typeBinding.getQualifiedName();
return VIEWER_SORTER.equals(qualifiedName)
|| TREEPATH_VIEWER_SORTER.equals(qualifiedName)
|| COMMON_VIEWER_SORTER.equals(qualifiedName);
}
/**
* Checks if a type is one of the deprecated ViewerSorter types based on the Type node.
* This method handles both cases: when type binding is available and when it's not.
*
* @param type the Type AST node to check
* @return {@code true} if it's a ViewerSorter type, {@code false} otherwise
*/
private static boolean isViewerSorterType(Type type) {
if (type == null) {
return false;
}
// First try to resolve the binding (most reliable)
ITypeBinding typeBinding = type.resolveBinding();
if (typeBinding != null && !typeBinding.isRecovered()) {
// Only trust the binding if it's fully resolved
return isViewerSorterType(typeBinding);
}
// Fallback: check by type name string (for when bindings aren't available or are recovered/incomplete)
String typeName = getTypeName(type);
return isViewerSorterTypeName(typeName);
}
/**
* Extracts the type name from a Type AST node.
*
* @param type the Type AST node
* @return the simple or qualified type name, or null if not determinable
*/
private static String getTypeName(Type type) {
if (type == null) {
return null;
}
if (type.isSimpleType()) {
return ((org.eclipse.jdt.core.dom.SimpleType) type).getName().getFullyQualifiedName();
}
if (type.isQualifiedType()) {
return ((org.eclipse.jdt.core.dom.QualifiedType) type).getName().getFullyQualifiedName();
}
if (type.isNameQualifiedType()) {
return ((org.eclipse.jdt.core.dom.NameQualifiedType) type).getName().getFullyQualifiedName();
}
return type.toString();
}
/**
* Checks if a type name (simple or qualified) matches a ViewerSorter type.
*
* @param typeName the type name to check
* @return {@code true} if it matches a ViewerSorter type, {@code false} otherwise
*/
private static boolean isViewerSorterTypeName(String typeName) {
if (typeName == null) {
return false;
}
// Check both simple and qualified names
return "ViewerSorter".equals(typeName) //$NON-NLS-1$
|| VIEWER_SORTER.equals(typeName)
|| "TreePathViewerSorter".equals(typeName) //$NON-NLS-1$
|| TREEPATH_VIEWER_SORTER.equals(typeName)
|| "CommonViewerSorter".equals(typeName) //$NON-NLS-1$
|| COMMON_VIEWER_SORTER.equals(typeName);
}
/**
* Gets the replacement type name for a given ViewerSorter type.
* Works with both resolved bindings and type name strings.
*
* @param type the Type AST node
* @return the replacement qualified type name, or null if not a ViewerSorter type
*/
private static String getReplacementTypeName(Type type) {
if (type == null) {
return null;
}
// First try to resolve the binding (only if fully resolved)
ITypeBinding typeBinding = type.resolveBinding();
if (typeBinding != null && !typeBinding.isRecovered()) {
return getReplacementQualifiedTypeName(typeBinding);
}
// Fallback: determine replacement based on type name string
String typeName = getTypeName(type);
if (typeName == null) {
return null;
}
if ("ViewerSorter".equals(typeName) || VIEWER_SORTER.equals(typeName)) { //$NON-NLS-1$
return VIEWER_COMPARATOR;
} else if ("TreePathViewerSorter".equals(typeName) || TREEPATH_VIEWER_SORTER.equals(typeName)) { //$NON-NLS-1$
return TREEPATH_VIEWER_COMPARATOR;
} else if ("CommonViewerSorter".equals(typeName) || COMMON_VIEWER_SORTER.equals(typeName)) { //$NON-NLS-1$
return COMMON_VIEWER_COMPARATOR;
}
return null;
}
/**
* Gets the fully qualified replacement type name for a deprecated ViewerSorter type.
*
* @param typeBinding the type binding of the deprecated type
* @return the fully qualified replacement type name
*/
private static String getReplacementQualifiedTypeName(ITypeBinding typeBinding) {
if (typeBinding == null) {
return null;
}
String qualifiedName = typeBinding.getQualifiedName();
if (VIEWER_SORTER.equals(qualifiedName)) {
return VIEWER_COMPARATOR;
} else if (TREEPATH_VIEWER_SORTER.equals(qualifiedName)) {
return TREEPATH_VIEWER_COMPARATOR;
} else if (COMMON_VIEWER_SORTER.equals(qualifiedName)) {
return COMMON_VIEWER_COMPARATOR;
}
return null;
}
/**
* Checks if a method is a JFace viewer getSorter() or setSorter() method.
*
* @param methodBinding the method binding to check
* @return {@code true} if it's a JFace viewer getSorter() or setSorter() method, {@code false} otherwise
*/
private static boolean isJFaceViewerSorterMethod(IMethodBinding methodBinding) {
if (methodBinding == null) {
return false;
}
String methodName = methodBinding.getName();
// Check method name is getSorter or setSorter
if ("getSorter".equals(methodName)) { //$NON-NLS-1$
// getSorter should have no parameters
if (methodBinding.getParameterTypes().length != 0) {
return false;
}
} else if ("setSorter".equals(methodName)) { //$NON-NLS-1$
// setSorter should have exactly one parameter
if (methodBinding.getParameterTypes().length != 1) {
return false;
}
} else {
return false;
}
// Check declaring class is a JFace viewer
ITypeBinding declaringClass = methodBinding.getDeclaringClass();
if (declaringClass == null) {
return false;
}
// Check if declaring class or any of its supertypes is a JFace viewer
return isJFaceViewer(declaringClass);
}
/**
* Checks if a method invocation is a JFace viewer getSorter() or setSorter() method
* based on the expression type name (fallback when bindings are not available).
*
* @param node the method invocation to check
* @return {@code true} if it appears to be a JFace viewer sorter method, {@code false} otherwise
*/
private static boolean isJFaceViewerSorterMethodByName(MethodInvocation node) {
if (node == null || node.getExpression() == null) {
return false;
}
// Try to get the type of the expression
org.eclipse.jdt.core.dom.Expression expr = node.getExpression();
ITypeBinding exprType = expr.resolveTypeBinding();
// Only trust fully resolved bindings
if (exprType != null && !exprType.isRecovered()) {
return isJFaceViewer(exprType);
}
// Fallback: check by variable name or type name
if (expr instanceof SimpleName simpleName) {
String name = simpleName.getIdentifier();
// Heuristic: if the variable name contains "viewer" (case-insensitive),
// it's likely a viewer
if (name.toLowerCase().contains("viewer")) { //$NON-NLS-1$
return true;
}
}
// Additional fallback: check if the type name (from recovered binding) contains "Viewer"
if (exprType != null && exprType.isRecovered()) {
String typeName = exprType.getName();
if (typeName != null && typeName.contains("Viewer")) { //$NON-NLS-1$
return true;
}
}
return false;
}
/**
* Checks if a type is a JFace viewer (or subtype).
*
* @param typeBinding the type binding to check
* @return {@code true} if it's a JFace viewer type, {@code false} otherwise
*/
private static boolean isJFaceViewer(ITypeBinding typeBinding) {
if (typeBinding == null) {
return false;
}
String qualifiedName = typeBinding.getQualifiedName();
// Check common JFace viewer types
if (qualifiedName.startsWith("org.eclipse.jface.viewers.")) { //$NON-NLS-1$
if (qualifiedName.equals("org.eclipse.jface.viewers.StructuredViewer") //$NON-NLS-1$
|| qualifiedName.equals("org.eclipse.jface.viewers.ContentViewer") //$NON-NLS-1$
|| qualifiedName.equals("org.eclipse.jface.viewers.TableViewer") //$NON-NLS-1$
|| qualifiedName.equals("org.eclipse.jface.viewers.TreeViewer") //$NON-NLS-1$
|| qualifiedName.equals("org.eclipse.jface.viewers.ListViewer") //$NON-NLS-1$
|| qualifiedName.equals("org.eclipse.jface.viewers.ComboViewer")) { //$NON-NLS-1$
return true;
}
}
// Check superclass
ITypeBinding superclass = typeBinding.getSuperclass();
if (superclass != null && isJFaceViewer(superclass)) {
return true;
}
return false;
}
/**
* Finds and identifies ViewerSorter usage patterns to be transformed.
*
* <p>This method scans the compilation unit for:</p>
* <ul>
* <li>Type declarations extending ViewerSorter classes</li>
* <li>Field declarations using ViewerSorter types</li>
* <li>Variable declarations using ViewerSorter types</li>
* <li>Method return types and parameters using ViewerSorter types</li>
* <li>ClassInstanceCreation of ViewerSorter types</li>
* <li>Cast expressions to ViewerSorter types</li>
* <li>getSorter() and setSorter() method invocations on JFace viewers</li>
* </ul>
*
* @param fixcore the cleanup fix core instance
* @param compilationUnit the compilation unit to analyze
* @param operations set to collect identified cleanup operations
* @param nodesprocessed set of nodes already processed to avoid duplicates
* @param createForOnlyIfVarUsed flag to control when operations are created (unused in this implementation)
*/
@Override
public void find(JfaceCleanUpFixCore fixcore, CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed,
boolean createForOnlyIfVarUsed) {
ReferenceHolder<Integer, SorterHolder> dataholder = new ReferenceHolder<>();
SorterHolder holder = new SorterHolder();
holder.nodesprocessed = nodesprocessed;
dataholder.put(0, holder);
// Each visitor type must be a separate AstProcessorBuilder call because
// chaining multiple onXxx calls creates sequential/scoped visitors via ASTProcessor,
// but these visitors need to run independently on the full compilation unit.
AstProcessorBuilder.with(dataholder, nodesprocessed)
.onTypeDeclaration((node, h) -> {
// Check extends clause
Type superclassType = node.getSuperclassType();
if (superclassType != null && isViewerSorterType(superclassType)) {
holder.typesToReplace.add(superclassType);
}
return true;
})
.build(compilationUnit);
AstProcessorBuilder.with(dataholder, nodesprocessed)
.onFieldDeclaration((node, h) -> {
Type fieldType = node.getType();
if (fieldType != null && isViewerSorterType(fieldType)) {
holder.typesToReplace.add(fieldType);
}
return true;
})
.build(compilationUnit);
AstProcessorBuilder.with(dataholder, nodesprocessed)
.onVariableDeclarationStatement((node, h) -> {
Type variableType = node.getType();
if (variableType != null && isViewerSorterType(variableType)) {
holder.typesToReplace.add(variableType);
}
return true;
})
.build(compilationUnit);
AstProcessorBuilder.with(dataholder, nodesprocessed)
.onMethodDeclaration((node, h) -> {
// Check return type
Type returnType = node.getReturnType2();
if (returnType != null && isViewerSorterType(returnType)) {
holder.typesToReplace.add(returnType);
}
return true;
})
.build(compilationUnit);
AstProcessorBuilder.with(dataholder, nodesprocessed)
.onSingleVariableDeclaration((node, h) -> {
// Check parameter type
Type paramType = node.getType();
if (paramType != null && isViewerSorterType(paramType)) {
holder.typesToReplace.add(paramType);
}
return true;
})
.build(compilationUnit);
AstProcessorBuilder.with(dataholder, nodesprocessed)
.onClassInstanceCreation((node, h) -> {
Type instanceType = node.getType();
if (instanceType != null && isViewerSorterType(instanceType)) {
holder.typesToReplace.add(instanceType);
}
return true;
})
.build(compilationUnit);
AstProcessorBuilder.with(dataholder, nodesprocessed)
.onCastExpression((node, h) -> {
Type castType = node.getType();
if (castType != null && isViewerSorterType(castType)) {
holder.typesToReplace.add(castType);
}
return true;
})
.build(compilationUnit);
AstProcessorBuilder.with(dataholder, nodesprocessed)
.onMethodInvocation((node, h) -> {
SimpleName methodName = node.getName();
if (methodName != null) {
String name = methodName.getIdentifier();
if ("getSorter".equals(name) || "setSorter".equals(name)) { //$NON-NLS-1$ //$NON-NLS-2$
// Check if this is a JFace viewer method - first try binding, then fallback to name-based check
IMethodBinding methodBinding = node.resolveMethodBinding();
if (isJFaceViewerSorterMethod(methodBinding) || isJFaceViewerSorterMethodByName(node)) {
holder.methodNamesToReplace.add(methodName);
}
}
}
return true;
})
.build(compilationUnit);
// If we found anything to replace, register the operation
if (!holder.typesToReplace.isEmpty() || !holder.methodNamesToReplace.isEmpty()) {
operations.add(fixcore.rewrite(dataholder));
}
}
/**
* Rewrites AST nodes to transform ViewerSorter patterns to ViewerComparator.
*
* <p>Performs transformations on:</p>
* <ol>
* <li>All Type nodes that reference ViewerSorter classes</li>
* <li>All method names that are getSorter() or setSorter() calls on JFace viewers</li>
* </ol>
*
* <p>The transformation ensures:</p>
* <ul>
* <li>Correct type replacement based on the original type</li>
* <li>Removal of old imports</li>
* <li>Addition of new imports</li>
* <li>Method name changes (getSorter → getComparator, setSorter → setComparator)</li>
* </ul>
*
* @param upp the cleanup fix core instance
* @param hit the holder containing identified ViewerSorter patterns to transform
* @param cuRewrite the compilation unit rewrite context
* @param group the text edit group for tracking changes
*/
@Override
public void rewrite(JfaceCleanUpFixCore upp, final ReferenceHolder<Integer, SorterHolder> hit,
final CompilationUnitRewrite cuRewrite, TextEditGroup group) {
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getRoot().getAST();
ImportRewrite importRewrite = cuRewrite.getImportRewrite();
if (hit.isEmpty()) {
return;
}
SorterHolder holder = hit.get(0);
// Replace all types
for (Type typeToReplace : holder.typesToReplace) {
if (!holder.nodesprocessed.contains(typeToReplace)) {
holder.nodesprocessed.add(typeToReplace);
// Use the new helper that handles null bindings
String replacementQualifiedName = getReplacementTypeName(typeToReplace);
if (replacementQualifiedName != null) {
// Create new type with proper import
Name newTypeName = addImport(replacementQualifiedName, cuRewrite, ast);
Type newType = ast.newSimpleType(newTypeName);
// Replace the type
rewrite.replace(typeToReplace, newType, group);
// Remove old import (if binding is available and fully resolved)
ITypeBinding typeBinding = typeToReplace.resolveBinding();
if (typeBinding != null && !typeBinding.isRecovered()) {
String oldQualifiedName = typeBinding.getQualifiedName();
importRewrite.removeImport(oldQualifiedName);
} else {
// Fallback: remove import based on type name
String typeName = getTypeName(typeToReplace);
if (typeName != null) {
// Try to remove both simple and qualified versions
if ("ViewerSorter".equals(typeName)) { //$NON-NLS-1$
importRewrite.removeImport(VIEWER_SORTER);
} else if ("TreePathViewerSorter".equals(typeName)) { //$NON-NLS-1$
importRewrite.removeImport(TREEPATH_VIEWER_SORTER);
} else if ("CommonViewerSorter".equals(typeName)) { //$NON-NLS-1$
importRewrite.removeImport(COMMON_VIEWER_SORTER);
} else {
// Already qualified name
importRewrite.removeImport(typeName);
}
}
}
}
}
}
// Replace all method names
for (Name methodNameToReplace : holder.methodNamesToReplace) {
if (!holder.nodesprocessed.contains(methodNameToReplace)) {
holder.nodesprocessed.add(methodNameToReplace);
// Determine the replacement method name based on the original
String originalName = methodNameToReplace.toString();
String replacementName;
if ("getSorter".equals(originalName)) { //$NON-NLS-1$
replacementName = "getComparator"; //$NON-NLS-1$
} else if ("setSorter".equals(originalName)) { //$NON-NLS-1$
replacementName = "setComparator"; //$NON-NLS-1$
} else {
// Should not happen, but keep original name as fallback
replacementName = originalName;
}
// Create new method name
SimpleName newMethodName = ast.newSimpleName(replacementName);
// Replace the method name
rewrite.replace(methodNameToReplace, newMethodName, group);
}
}
}
@Override
public String getPreview(boolean afterRefactoring) {
if (!afterRefactoring) {
return """
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.jface.viewers.TableViewer;
public class MyClass extends ViewerSorter {
private ViewerSorter sorter;
public void configure(TableViewer viewer) {
viewer.setSorter(new ViewerSorter());
ViewerSorter s = viewer.getSorter();
}
}
"""; //$NON-NLS-1$
}
return """
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.TableViewer;
public class MyClass extends ViewerComparator {
private ViewerComparator sorter;
public void configure(TableViewer viewer) {
viewer.setComparator(new ViewerComparator());
ViewerComparator s = viewer.getComparator();
}
}
"""; //$NON-NLS-1$
}
}