JFacePlugin.java
/*******************************************************************************
* Copyright (c) 2021 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.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
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.osgi.framework.Bundle;
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 {@link SubProgressMonitor} to {@link SubMonitor}.
*
* <p>This helper transforms progress monitor usage patterns in Eclipse JDT code:</p>
* <ul>
* <li>Converts {@code IProgressMonitor.beginTask()} to {@code SubMonitor.convert()}</li>
* <li>Replaces {@code new SubProgressMonitor(monitor, work)} with {@code subMonitor.split(work)}</li>
* <li>Handles both 2-argument and 3-argument SubProgressMonitor constructors</li>
* <li>Generates unique variable names to avoid collisions in scope</li>
* </ul>
*
* <p><b>Migration Pattern:</b></p>
* <pre>
* // Before:
* monitor.beginTask("Main Task", 100);
* IProgressMonitor subMon = new SubProgressMonitor(monitor, 60);
*
* // After:
* SubMonitor subMonitor = SubMonitor.convert(monitor, "Main Task", 100);
* IProgressMonitor subMon = subMonitor.split(60);
* </pre>
*
* @see SubProgressMonitor
* @see SubMonitor
*/
public class JFacePlugin extends
AbstractTool<ReferenceHolder<Integer, JFacePlugin.MonitorHolder>> {
public static final String CLASS_INSTANCE_CREATION = "ClassInstanceCreation"; //$NON-NLS-1$
public static final String METHODINVOCATION = "MethodInvocation"; //$NON-NLS-1$
/** Debug option key for enabling JFace plugin transformation logging */
private static final String DEBUG_OPTION = "sandbox_jface_cleanup/debug/jfaceplugin"; //$NON-NLS-1$
/** Bundle symbolic name for logging */
private static final String BUNDLE_ID = "sandbox_jface_cleanup"; //$NON-NLS-1$
/**
* Holder for monitor-related transformation data.
* Tracks beginTask invocations and associated SubProgressMonitor instances.
*/
public static class MonitorHolder {
/** The beginTask method invocation to be converted */
public MethodInvocation minv;
/** The monitor variable name from beginTask expression */
public String minvname;
/** Set of SubProgressMonitor constructions to be converted to split() calls */
public Set<ClassInstanceCreation> setofcic = new HashSet<>();
/** Nodes that have been processed to avoid duplicate transformations */
public Set<ASTNode> nodesprocessed;
}
/**
* Checks if debug logging is enabled for JFace plugin transformations.
*
* @return {@code true} if debug logging is enabled, {@code false} otherwise
*/
private static boolean isDebugEnabled() {
return Platform.inDebugMode() && "true".equalsIgnoreCase(Platform.getDebugOption(DEBUG_OPTION)); //$NON-NLS-1$
}
/**
* Logs a debug message if debug mode is enabled.
*
* @param message the message to log
*/
private static void logDebug(String message) {
if (isDebugEnabled()) {
try {
Bundle bundle = Platform.getBundle(BUNDLE_ID);
if (bundle != null) {
ILog log = Platform.getLog(bundle);
log.log(new Status(IStatus.INFO, BUNDLE_ID, "JFacePlugin: " + message)); //$NON-NLS-1$
}
} catch (Exception e) {
System.err.println("Failed to log debug message: " + e.getMessage());
e.printStackTrace(System.err);
}
}
}
/**
* Finds and identifies SubProgressMonitor usage patterns to be transformed.
*
* <p>This method scans the compilation unit for:</p>
* <ul>
* <li>{@code beginTask} method invocations on IProgressMonitor instances</li>
* <li>{@code SubProgressMonitor} constructor invocations that reference the same monitor</li>
* </ul>
*
* <p>When both patterns are found in the same scope, a cleanup operation is registered
* to transform them to the SubMonitor pattern.</p>
*
* @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, MonitorHolder> dataholder = new ReferenceHolder<>();
AstProcessorBuilder.with(dataholder, nodesprocessed)
.processor()
.callMethodInvocationVisitor(IProgressMonitor.class, "beginTask", (node, holder) -> { //$NON-NLS-1$
if (node.arguments().size() != 2) {
return true;
}
logDebug("Found beginTask at position " + node.getStartPosition() + " (type: " + node.getClass().getSimpleName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
// Check if parent is ExpressionStatement, otherwise skip
if (!(node.getParent() instanceof ExpressionStatement)) {
return true;
}
Expression expr = node.getExpression();
if (expr == null) {
return true;
}
SimpleName sn = ASTNodes.as(expr, SimpleName.class);
if (sn != null) {
IBinding ibinding = sn.resolveBinding();
// Add null-check for binding
if (ibinding == null) {
return true;
}
String name = ibinding.getName();
MonitorHolder mh = new MonitorHolder();
mh.minv = node;
mh.minvname = name;
mh.nodesprocessed = nodesprocessed;
holder.put(holder.size(), mh);
}
return true;
}, s -> ASTNodes.getTypedAncestor(s, Block.class))
.callClassInstanceCreationVisitor(SubProgressMonitor.class, (node, holder) -> {
// Guard against empty holder
if (holder.isEmpty()) {
return true;
}
MonitorHolder mh = holder.get(holder.size() - 1);
List<?> arguments = node.arguments();
if (arguments.isEmpty()) {
return true;
}
// Safe handling of first argument - extract identifier from expression
Expression firstArg = (Expression) arguments.get(0);
String firstArgName = null;
// Try to extract SimpleName from the expression
SimpleName sn = ASTNodes.as(firstArg, SimpleName.class);
if (sn != null) {
firstArgName = sn.getIdentifier();
}
if (firstArgName == null || !mh.minvname.equals(firstArgName)) {
return true;
}
logDebug("Found SubProgressMonitor construction at position " + node.getStartPosition() + " for variable '" + firstArgName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
mh.setofcic.add(node);
operations.add(fixcore.rewrite(holder));
return true;
})
.build(compilationUnit);
}
/**
* Rewrites AST nodes to transform SubProgressMonitor patterns to SubMonitor.
*
* <p>Performs two main transformations:</p>
* <ol>
* <li><b>beginTask → convert:</b> Transforms {@code monitor.beginTask(msg, work)}
* to {@code SubMonitor subMonitor = SubMonitor.convert(monitor, msg, work)}</li>
* <li><b>SubProgressMonitor → split:</b> Transforms constructor calls:
* <ul>
* <li>2-arg: {@code new SubProgressMonitor(monitor, work)} → {@code subMonitor.split(work)}</li>
* <li>3-arg: {@code new SubProgressMonitor(monitor, work, flags)} → {@code subMonitor.split(work, flags)}</li>
* </ul>
* </li>
* </ol>
*
* <p>The transformation ensures:</p>
* <ul>
* <li>Unique variable names for SubMonitor to avoid collisions</li>
* <li>Preservation of flags parameter in 3-arg constructors</li>
* <li>Removal of SubProgressMonitor import</li>
* <li>Addition of SubMonitor import</li>
* </ul>
*
* @param upp the cleanup fix core instance
* @param hit the holder containing identified monitor 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, MonitorHolder> hit,
final CompilationUnitRewrite cuRewrite, TextEditGroup group) {
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getRoot().getAST();
ImportRewrite importRemover = cuRewrite.getImportRewrite();
// Guard against empty holder
if (hit.isEmpty()) {
return;
}
Set<ASTNode> nodesprocessed = hit.get(hit.size() - 1).nodesprocessed;
for (Entry<Integer, MonitorHolder> entry : hit.entrySet()) {
MonitorHolder mh = entry.getValue();
MethodInvocation minv = mh.minv;
// Generate unique identifier name for SubMonitor variable
String identifier = generateUniqueVariableName(minv, "subMonitor"); //$NON-NLS-1$
if (!nodesprocessed.contains(minv)) {
nodesprocessed.add(minv);
logDebug("Rewriting beginTask at position " + minv.getStartPosition() + " (method: " + minv.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
// Ensure parent is ExpressionStatement
if (!(minv.getParent() instanceof ExpressionStatement)) {
continue;
}
List<ASTNode> arguments = minv.arguments();
/**
* Here we process the "beginTask" and change it to "SubMonitor.convert"
*
* monitor.beginTask(NewWizardMessages.NewSourceFolderWizardPage_operation, 3);
* SubMonitor subMonitor =
* SubMonitor.convert(monitor,NewWizardMessages.NewSourceFolderWizardPage_operation,
* 3);
*
*/
SingleVariableDeclaration newVariableDeclarationStatement = ast.newSingleVariableDeclaration();
newVariableDeclarationStatement.setName(ast.newSimpleName(identifier));
newVariableDeclarationStatement
.setType(ast.newSimpleType(addImport(SubMonitor.class.getCanonicalName(), cuRewrite, ast)));
MethodInvocation staticCall = ast.newMethodInvocation();
staticCall.setExpression(ASTNodeFactory.newName(ast, SubMonitor.class.getSimpleName()));
staticCall.setName(ast.newSimpleName("convert")); //$NON-NLS-1$
List<ASTNode> staticCallArguments = staticCall.arguments();
staticCallArguments.add(
ASTNodes.createMoveTarget(rewrite, ASTNodes.getUnparenthesedExpression(minv.getExpression())));
staticCallArguments
.add(ASTNodes.createMoveTarget(rewrite, ASTNodes.getUnparenthesedExpression(arguments.get(0))));
staticCallArguments
.add(ASTNodes.createMoveTarget(rewrite, ASTNodes.getUnparenthesedExpression(arguments.get(1))));
newVariableDeclarationStatement.setInitializer(staticCall);
ASTNodes.replaceButKeepComment(rewrite, minv, newVariableDeclarationStatement, group);
logDebug("Created SubMonitor.convert call: " + staticCall); //$NON-NLS-1$
}
for (ClassInstanceCreation submon : mh.setofcic) {
List<?> arguments = submon.arguments();
if (arguments.size() < 2) {
continue;
}
ASTNode origarg = (ASTNode) arguments.get(1);
logDebug("Rewriting SubProgressMonitor at position " + submon.getStartPosition() + " (ClassInstanceCreation)"); //$NON-NLS-1$ //$NON-NLS-2$
/**
* Handle both 2-arg and 3-arg SubProgressMonitor constructors:
*
* 2-arg: new SubProgressMonitor(monitor, work)
* -> subMonitor.split(work)
*
* 3-arg: new SubProgressMonitor(monitor, work, flags)
* -> subMonitor.split(work, flags)
*/
MethodInvocation newMethodInvocation2 = ast.newMethodInvocation();
newMethodInvocation2.setName(ast.newSimpleName("split")); //$NON-NLS-1$
newMethodInvocation2.setExpression(ASTNodeFactory.newName(ast, identifier));
List<ASTNode> splitCallArguments = newMethodInvocation2.arguments();
// Add the work amount (second argument)
splitCallArguments
.add(ASTNodes.createMoveTarget(rewrite, ASTNodes.getUnparenthesedExpression(origarg)));
// Check for 3-arg constructor (with flags)
if (arguments.size() >= 3) {
ASTNode flagsArg = (ASTNode) arguments.get(2);
splitCallArguments.add(ASTNodes.createMoveTarget(rewrite, ASTNodes.getUnparenthesedExpression(flagsArg)));
}
ASTNodes.replaceButKeepComment(rewrite, submon, newMethodInvocation2, group);
importRemover.removeImport(SubProgressMonitor.class.getCanonicalName());
}
}
}
/**
* Generates a unique variable name that doesn't collide with existing variables in scope.
*
* <p>This method ensures the SubMonitor variable name doesn't conflict with other
* variables visible at the transformation point. If the base name is already in use,
* a numeric suffix is appended (e.g., "subMonitor2", "subMonitor3", etc.).</p>
*
* @param node the AST node context for scope analysis
* @param baseName the base name to use (e.g., "subMonitor")
* @return a unique variable name that doesn't exist in the current scope
*/
private String generateUniqueVariableName(ASTNode node, String baseName) {
Collection<String> usedNames = getUsedVariableNames(node);
// If base name is not used, return it
if (!usedNames.contains(baseName)) {
return baseName;
}
// Otherwise, append a number until we find an unused name
int counter = 2;
String candidate = baseName + counter;
while (usedNames.contains(candidate)) {
counter++;
candidate = baseName + counter;
}
return candidate;
}
@Override
public String getPreview(boolean afterRefactoring) {
if (!afterRefactoring) {
return """
monitor.beginTask(NewWizardMessages.NewSourceFolderWizardPage_operation, 3);
IProgressMonitor subProgressMonitor= new SubProgressMonitor(monitor, 1);
IProgressMonitor subProgressMonitor2= new SubProgressMonitor(monitor, 2);
"""; //$NON-NLS-1$
}
return """
SubMonitor subMonitor=SubMonitor.convert(monitor,NewWizardMessages.NewSourceFolderWizardPage_operation,3);
IProgressMonitor subProgressMonitor= subMonitor.split(1);
IProgressMonitor subProgressMonitor2= subMonitor.split(2);
"""; //$NON-NLS-1$
}
}