ImageDataProviderPlugin.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.List;
import java.util.Set;
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.CastExpression;
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.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
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.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 {@code Image(Device, ImageData)} to {@code Image(Device, ImageDataProvider)}.
*
* <p>This helper transforms image creation patterns in Eclipse SWT code to support DPI/zoom awareness:</p>
* <ul>
* <li>Converts {@code new Image(device, imageData)} to {@code new Image(device, (ImageDataProvider) zoom -> {...})}</li>
* <li>Inlines ImageData creation into lambda for zoom-aware scaling</li>
* <li>Removes unused local ImageData variable declarations</li>
* </ul>
*
* <p><b>Migration Pattern:</b></p>
* <pre>
* // Before:
* ImageData imageData = new ImageData(1, 1, 1, palette);
* Image image = new Image(device, imageData);
*
* // After:
* Image image = new Image(device, (ImageDataProvider) zoom -> {
* return new ImageData(1, 1, 1, palette);
* });
* </pre>
*
* @see org.eclipse.swt.graphics.Image
* @see org.eclipse.swt.graphics.ImageData
* @see org.eclipse.swt.graphics.ImageDataProvider
* @see <a href="https://github.com/eclipse-platform/eclipse.platform.ui/pull/3004">Eclipse Platform UI PR #3004</a>
*/
public class ImageDataProviderPlugin extends
AbstractTool<ReferenceHolder<Integer, ImageDataProviderPlugin.ImageDataHolder>> {
/** SWT Device class */
private static final String DEVICE = "org.eclipse.swt.graphics.Device"; //$NON-NLS-1$
/** SWT Image class */
private static final String IMAGE = "org.eclipse.swt.graphics.Image"; //$NON-NLS-1$
/** SWT ImageData class */
private static final String IMAGE_DATA = "org.eclipse.swt.graphics.ImageData"; //$NON-NLS-1$
/** SWT ImageDataProvider interface */
private static final String IMAGE_DATA_PROVIDER = "org.eclipse.swt.graphics.ImageDataProvider"; //$NON-NLS-1$
/**
* Holder for Image creation transformation data.
* Tracks Image constructor calls that can be transformed to use ImageDataProvider.
*/
public static class ImageDataHolder {
/** ClassInstanceCreation of Image(Device, ImageData) to transform */
public ClassInstanceCreation imageCreation;
/** ImageData variable declaration statement to remove (if applicable) */
public VariableDeclarationStatement imageDataVarDecl;
/** ImageData initialization expression to inline into lambda */
public Expression imageDataInitializer;
/** Nodes that have been processed to avoid duplicate transformations */
public Set<ASTNode> nodesprocessed;
}
/**
* Finds and identifies Image(Device, ImageData) patterns to be transformed.
*
* <p>This method scans the compilation unit for:</p>
* <ul>
* <li>ClassInstanceCreation matching {@code new Image(device, imageData)}</li>
* <li>ImageData argument is a local variable with an initializer</li>
* <li>ImageData variable is only used in the Image constructor</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<String, Object> findHolder = ReferenceHolder.create();
AstProcessorBuilder.with(findHolder)
.onClassInstanceCreation((node, h) -> {
// Check if this is a new Image(...) creation
ITypeBinding typeBinding = node.getType().resolveBinding();
if (typeBinding == null || !IMAGE.equals(typeBinding.getQualifiedName())) {
return true;
}
// Check if it has exactly 2 arguments
@SuppressWarnings("unchecked")
List<Expression> arguments = node.arguments();
if (arguments.size() != 2) {
return true;
}
// Check first argument is Device (or subtype)
Expression deviceArg = arguments.get(0);
ITypeBinding deviceType = deviceArg.resolveTypeBinding();
if (deviceType == null || !isOfType(deviceType, DEVICE)) {
return true;
}
// Check second argument is ImageData
Expression imageDataArg = arguments.get(1);
ITypeBinding imageDataType = imageDataArg.resolveTypeBinding();
if (imageDataType == null || !IMAGE_DATA.equals(imageDataType.getQualifiedName())) {
return true;
}
// Check if imageDataArg is a SimpleName (local variable reference)
if (!(imageDataArg instanceof SimpleName)) {
return true;
}
SimpleName imageDataVar = (SimpleName) imageDataArg;
// Try to find the variable declaration in the same method/block
VariableDeclarationStatement varDecl = findImageDataVarDecl(imageDataVar);
if (varDecl == null) {
return true;
}
// Get the initializer expression
VariableDeclarationFragment fragment = findFragment(varDecl, imageDataVar);
if (fragment == null || fragment.getInitializer() == null) {
return true;
}
// Skip multi-fragment declarations (e.g., "ImageData a = ..., b = ...;")
// These are too complex to safely transform
@SuppressWarnings("unchecked")
List<VariableDeclarationFragment> declFragments = varDecl.fragments();
if (declFragments.size() != 1) {
return true;
}
// Check that the ImageData variable is referenced exactly once (excluding its declaration)
// This ensures it's safe to remove the variable declaration
int usageCount = countVariableReferences(imageDataVar);
if (usageCount != 1) {
return true;
}
// Create a transformation holder
ReferenceHolder<Integer, ImageDataHolder> dataholder = new ReferenceHolder<>();
ImageDataHolder holder = new ImageDataHolder();
holder.imageCreation = node;
holder.imageDataVarDecl = varDecl;
holder.imageDataInitializer = fragment.getInitializer();
holder.nodesprocessed = nodesprocessed;
dataholder.put(0, holder);
// Register the operation
operations.add(fixcore.rewrite(dataholder));
return true;
})
.build(compilationUnit);
}
/**
* Finds the VariableDeclarationStatement for a given variable name in the same method/block.
*
* @param varName the variable name to find
* @return the VariableDeclarationStatement or null if not found
*/
private VariableDeclarationStatement findImageDataVarDecl(SimpleName varName) {
// Navigate up to find the enclosing method or block
ASTNode parent = varName.getParent();
while (parent != null && !(parent instanceof MethodDeclaration) && !(parent instanceof Block)) {
parent = parent.getParent();
}
if (parent == null) {
return null;
}
// Search for the variable declaration
final VariableDeclarationStatement[] result = new VariableDeclarationStatement[1];
final String varIdentifier = varName.getIdentifier();
ReferenceHolder<String, Object> searchHolder = ReferenceHolder.create();
AstProcessorBuilder.with(searchHolder)
.onVariableDeclarationStatement((node, h) -> {
ITypeBinding typeBinding = node.getType().resolveBinding();
if (typeBinding != null && IMAGE_DATA.equals(typeBinding.getQualifiedName())) {
@SuppressWarnings("unchecked")
List<VariableDeclarationFragment> fragments = node.fragments();
for (VariableDeclarationFragment fragment : fragments) {
if (fragment.getName().getIdentifier().equals(varIdentifier)) {
result[0] = node;
return false;
}
}
}
return true;
})
.build(parent);
return result[0];
}
/**
* Finds the VariableDeclarationFragment for a given variable name.
*
* @param varDecl the variable declaration statement
* @param varName the variable name to find
* @return the VariableDeclarationFragment or null if not found
*/
private VariableDeclarationFragment findFragment(VariableDeclarationStatement varDecl, SimpleName varName) {
@SuppressWarnings("unchecked")
List<VariableDeclarationFragment> fragments = varDecl.fragments();
for (VariableDeclarationFragment fragment : fragments) {
if (fragment.getName().getIdentifier().equals(varName.getIdentifier())) {
return fragment;
}
}
return null;
}
/**
* Counts how many times a variable is referenced in the enclosing method.
*
* @param varName the SimpleName node of the variable to count references for
* @return the number of references to the variable (excluding the declaration itself)
*/
private int countVariableReferences(SimpleName varName) {
// Navigate up to find the enclosing method
ASTNode parent = varName.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
if (parent == null) {
return 0;
}
// Get the binding of the variable we're looking for
final IBinding varBinding = varName.resolveBinding();
if (varBinding == null) {
return 0;
}
// Count references to the variable (excluding the declaration)
final int[] count = new int[1];
ReferenceHolder<String, Object> countHolder = ReferenceHolder.create();
AstProcessorBuilder.with(countHolder)
.onSimpleName((node, h) -> {
// Check if this node refers to the same variable binding
IBinding nodeBinding = node.resolveBinding();
if (nodeBinding != null && nodeBinding.equals(varBinding)) {
// Check if this is a reference (not the declaration itself)
ASTNode nodeParent = node.getParent();
// Exclude the name in the VariableDeclarationFragment (the declaration)
if (nodeParent instanceof VariableDeclarationFragment) {
VariableDeclarationFragment fragment = (VariableDeclarationFragment) nodeParent;
// Only count if it's not the name being declared
if (!node.equals(fragment.getName())) {
count[0]++;
}
} else {
// All other occurrences are references
count[0]++;
}
}
return true;
})
.build(parent);
return count[0];
}
/**
* Rewrites AST nodes to transform Image creation to use ImageDataProvider.
*
* <p>Performs transformation:</p>
* <ol>
* <li>Replace {@code new Image(device, imageData)} with lambda-based ImageDataProvider</li>
* <li>Inline ImageData creation into lambda body</li>
* <li>Remove now-unused ImageData variable declaration</li>
* <li>Add ImageDataProvider import</li>
* </ol>
*
* @param upp the cleanup fix core instance
* @param hit the holder containing identified Image 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, ImageDataHolder> hit,
final CompilationUnitRewrite cuRewrite, TextEditGroup group) {
if (hit.isEmpty()) {
return;
}
ImageDataHolder holder = hit.get(0);
if (holder == null || holder.imageCreation == null) {
return;
}
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getAST();
// Get the device argument from the original Image creation
@SuppressWarnings("unchecked")
List<Expression> originalArgs = holder.imageCreation.arguments();
Expression deviceArg = originalArgs.get(0);
// Create lambda: zoom -> { return <imageDataInitializer>; }
LambdaExpression lambda = ast.newLambdaExpression();
// Lambda parameter: zoom
VariableDeclarationFragment zoomParam = ast.newVariableDeclarationFragment();
zoomParam.setName(ast.newSimpleName("zoom")); //$NON-NLS-1$
lambda.parameters().add(zoomParam);
// Lambda body: { return <imageDataInitializer>; }
Block lambdaBody = ast.newBlock();
ReturnStatement returnStmt = ast.newReturnStatement();
returnStmt.setExpression((Expression) ASTNode.copySubtree(ast, holder.imageDataInitializer));
lambdaBody.statements().add(returnStmt);
lambda.setBody(lambdaBody);
// Create cast: (ImageDataProvider) lambda
CastExpression castExpr = ast.newCastExpression();
castExpr.setType(ast.newSimpleType(addImport(IMAGE_DATA_PROVIDER, cuRewrite, ast)));
castExpr.setExpression(lambda);
// Create new Image creation: new Image(device, (ImageDataProvider) lambda)
ClassInstanceCreation newImageCreation = ast.newClassInstanceCreation();
newImageCreation.setType(ast.newSimpleType(ast.newSimpleName("Image"))); //$NON-NLS-1$
newImageCreation.arguments().add(ASTNodes.createMoveTarget(rewrite, deviceArg));
newImageCreation.arguments().add(castExpr);
// Replace the old Image creation with the new one
ASTNodes.replaceButKeepComment(rewrite, holder.imageCreation, newImageCreation, group);
// Remove the ImageData variable declaration if it exists
if (holder.imageDataVarDecl != null) {
// Check if this is the only fragment in the declaration
@SuppressWarnings("unchecked")
List<VariableDeclarationFragment> fragments = holder.imageDataVarDecl.fragments();
if (fragments.size() == 1) {
// Remove the entire statement
rewrite.remove(holder.imageDataVarDecl, group);
} else {
// Remove only the specific fragment (multiple variables declared)
// Find the fragment that matches our imageData variable
for (VariableDeclarationFragment fragment : fragments) {
if (fragment.getInitializer() == holder.imageDataInitializer) {
ListRewrite listRewrite = rewrite.getListRewrite(holder.imageDataVarDecl,
VariableDeclarationStatement.FRAGMENTS_PROPERTY);
listRewrite.remove(fragment, group);
break;
}
}
}
}
}
@Override
public String getPreview(boolean afterRefactoring) {
if (!afterRefactoring) {
return """
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
public class Test {
public Image createPattern(Device device) {
PaletteData palette = new PaletteData(new RGB(0, 0, 0));
ImageData imageData = new ImageData(1, 1, 1, palette);
Image image = new Image(device, imageData);
return image;
}
}
"""; //$NON-NLS-1$
}
return """
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageDataProvider;
import org.eclipse.swt.graphics.PaletteData;
public class Test {
public Image createPattern(Device device) {
PaletteData palette = new PaletteData(new RGB(0, 0, 0));
Image image = new Image(device, (ImageDataProvider) zoom -> {
return new ImageData(1, 1, 1, palette);
});
return image;
}
}
"""; //$NON-NLS-1$
}
}