InlineSequencesPlugin.java
/*******************************************************************************
* Copyright (c) 2025 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.core.runtime.CoreException;
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.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.Statement;
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.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.refactoring.util.TightSourceRangeComputer;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.HelperVisitor;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.MethodReuseCleanUpFixCore;
import org.sandbox.jdt.internal.corext.fix.helper.lib.AbstractMethodReuse;
import org.sandbox.jdt.internal.corext.fix.helper.lib.InlineCodeSequenceFinder;
import org.sandbox.jdt.internal.corext.fix.helper.lib.InlineCodeSequenceFinder.InlineSequenceMatch;
import org.sandbox.jdt.internal.corext.fix.helper.lib.MethodCallReplacer;
import org.sandbox.jdt.internal.corext.fix.helper.lib.VariableMapping;
/**
* Plugin for detecting and replacing inline code sequences with method calls
*/
public class InlineSequencesPlugin extends AbstractMethodReuse<MethodDeclaration> {
@Override
public void find(Object fixcore, CompilationUnit compilationUnit,
Set<?> operations, Set<ASTNode> nodesprocessed) throws CoreException {
// Cast to the correct type
@SuppressWarnings("unchecked")
Set<CompilationUnitRewriteOperation> ops = (Set<CompilationUnitRewriteOperation>) operations;
MethodReuseCleanUpFixCore fixCore = (MethodReuseCleanUpFixCore) fixcore;
// Use HelperVisitor to visit all methods in the compilation unit
ReferenceHolder<Integer, Object> dataholder = new ReferenceHolder<>();
HelperVisitor.callMethodDeclarationVisitor(compilationUnit, dataholder, nodesprocessed,
(node, holder) -> {
// Search for inline sequences matching this method
List<InlineSequenceMatch> matches = InlineCodeSequenceFinder.findInlineSequences(compilationUnit, node);
// Create operations for each match found
for (InlineSequenceMatch match : matches) {
List<Statement> matchingStatements = match.getMatchingStatements();
if (matchingStatements.isEmpty()) {
continue;
}
if (!nodesprocessed.contains(matchingStatements.get(0))) {
// Create a ReferenceHolder to store both the method and the match
ReferenceHolder<Integer, Object> matchHolder = new ReferenceHolder<>();
matchHolder.put(0, node);
matchHolder.put(1, match);
ops.add(fixCore.rewrite(matchHolder));
// Mark all statements in the match as processed
matchingStatements.forEach(nodesprocessed::add);
}
}
return false;
});
}
@Override
public void rewrite(Object fixcore, ReferenceHolder<?, ?> holder,
CompilationUnitRewrite cuRewrite, TextEditGroup group) throws CoreException {
// Cast to the correct types
MethodReuseCleanUpFixCore fixCore = (MethodReuseCleanUpFixCore) fixcore;
// Get the method declaration and match from the holder
MethodDeclaration targetMethod = (MethodDeclaration) holder.get(0);
InlineSequenceMatch match = (InlineSequenceMatch) holder.get(1);
if (targetMethod == null || match == null) {
return;
}
// Important: Get the AST from cuRewrite's root, not from the original nodes
AST ast = cuRewrite.getRoot().getAST();
ASTRewrite rewrite = cuRewrite.getASTRewrite();
// Set up tight source range computer
TightSourceRangeComputer rangeComputer;
if (rewrite.getExtendedSourceRangeComputer() instanceof TightSourceRangeComputer) {
rangeComputer = (TightSourceRangeComputer) rewrite.getExtendedSourceRangeComputer();
} else {
rangeComputer = new TightSourceRangeComputer();
}
List<Statement> statementsToReplace = match.getMatchingStatements();
if (!statementsToReplace.isEmpty()) {
rangeComputer.addTightSourceNode(statementsToReplace.get(0));
}
rewrite.setTargetSourceRangeComputer(rangeComputer);
// Create the method invocation
VariableMapping variableMapping = match.getVariableMapping();
MethodInvocation methodCall = MethodCallReplacer.createMethodCall(ast, targetMethod, variableMapping);
if (methodCall == null) {
return;
}
// Determine how to replace based on the target method's return type
replaceStatementsWithMethodCall(rewrite, ast, targetMethod, methodCall, statementsToReplace, group);
}
/**
* Replace the inline code sequence with a method call
* Handles both return statements and variable declarations
*/
@SuppressWarnings("unchecked")
private void replaceStatementsWithMethodCall(ASTRewrite rewrite, AST ast,
MethodDeclaration targetMethod, MethodInvocation methodCall,
List<Statement> statementsToReplace, TextEditGroup group) {
if (statementsToReplace.isEmpty()) {
return;
}
Statement firstStatement = statementsToReplace.get(0);
ASTNode parent = firstStatement.getParent();
if (!(parent instanceof Block)) {
return;
}
ListRewrite listRewrite = rewrite.getListRewrite(parent, Block.STATEMENTS_PROPERTY);
// Check if the target method has a return statement
// If so, we need to handle variable declaration specially
boolean targetReturnsValue = methodReturnsValue(targetMethod);
Statement lastInlineStatement = statementsToReplace.get(statementsToReplace.size() - 1);
if (targetReturnsValue && lastInlineStatement instanceof VariableDeclarationStatement) {
// The last statement is a variable declaration that corresponds to the return value
// Replace it with a variable declaration using the method call
VariableDeclarationStatement varDecl = (VariableDeclarationStatement) lastInlineStatement;
// Defensive check: ensure there is at least one fragment before accessing index 0
if (varDecl.fragments().isEmpty()) {
// Fallback to simple replacement with a method call expression
ExpressionStatement expressionStatement = ast.newExpressionStatement((Expression) ASTNode.copySubtree(ast, methodCall));
listRewrite.replace(firstStatement, expressionStatement, group);
// Remove remaining statements
for (int i = 1; i < statementsToReplace.size(); i++) {
listRewrite.remove(statementsToReplace.get(i), group);
}
return;
}
VariableDeclarationFragment fragment = (VariableDeclarationFragment) varDecl.fragments().get(0);
// Create new variable declaration with method call as initializer
VariableDeclarationFragment newFragment = ast.newVariableDeclarationFragment();
newFragment.setName(ast.newSimpleName(fragment.getName().getIdentifier()));
newFragment.setInitializer((Expression) ASTNode.copySubtree(ast, methodCall));
VariableDeclarationStatement newVarDecl = ast.newVariableDeclarationStatement(newFragment);
newVarDecl.setType((org.eclipse.jdt.core.dom.Type) ASTNode.copySubtree(ast, varDecl.getType()));
// Replace the first statement with the new variable declaration
listRewrite.replace(firstStatement, newVarDecl, group);
// Remove intermediate statements (but not the last one which we're effectively replacing)
for (int i = 1; i < statementsToReplace.size() - 1; i++) {
listRewrite.remove(statementsToReplace.get(i), group);
}
// Remove the last statement
if (statementsToReplace.size() > 1) {
listRewrite.remove(lastInlineStatement, group);
}
} else {
// Simple case: just replace with expression statement
ExpressionStatement expressionStatement = ast.newExpressionStatement((Expression) ASTNode.copySubtree(ast, methodCall));
listRewrite.replace(firstStatement, expressionStatement, group);
// Remove remaining statements
for (int i = 1; i < statementsToReplace.size(); i++) {
listRewrite.remove(statementsToReplace.get(i), group);
}
}
}
/**
* Check if a method returns a value (has a return statement with an expression)
*/
@SuppressWarnings("unchecked")
private boolean methodReturnsValue(MethodDeclaration method) {
if (method.getBody() == null) {
return false;
}
List<Statement> statements = method.getBody().statements();
if (statements.isEmpty()) {
return false;
}
// Check the last statement
Statement lastStatement = statements.get(statements.size() - 1);
if (lastStatement instanceof ReturnStatement) {
ReturnStatement returnStmt = (ReturnStatement) lastStatement;
return returnStmt.getExpression() != null;
}
return false;
}
@Override
public String getPreview(boolean afterRefactoring) {
if (afterRefactoring) {
return """
// After: Inline code replaced with method call
String name = formatName(firstName, lastName);
System.out.println(name);
""";
} else {
return """
// Before: Duplicated code inline
String name = firstName.trim() + " " + lastName.trim();
System.out.println(name);
""";
}
}
}