ChangeVariableGeneralizer.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
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.internal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.jdt.core.JavaCore;
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.BreakStatement;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.ExpressionMethodReference;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.LabeledStatement;
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.SuperFieldAccess;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.SuperMethodReference;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeMethodReference;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.sandbox.jdt.triggerpattern.internal.RuleAuthoringDocument.ChangeExample;
/**
* Syntactic suggestions for explicit author confirmation. These are variable-shaped
* names, not inferred bindings or a proof of an API migration's semantic scope.
* Methods, types, member selectors, labels, comments and literals are left intact.
*/
public final class ChangeVariableGeneralizer {
private ChangeVariableGeneralizer() {
}
/** Returns shared variable-shaped names; none is selected automatically. */
public static List<String> candidates(ChangeExample example) {
Map<String, List<SimpleName>> before = variableNames(example.before());
before.keySet().retainAll(variableNames(example.after()).keySet());
return List.copyOf(before.keySet());
}
/** Replaces only AST occurrences belonging to explicitly selected suggestions. */
public static ChangeExample generalize(ChangeExample example, Set<String> selected) {
if (!candidates(example).containsAll(selected)) {
throw new IllegalArgumentException("Only suggested variable names may be generalized"); //$NON-NLS-1$
}
return new ChangeExample(replace(example.before(), selected), replace(example.after(), selected));
}
private static String replace(String code, Set<String> selected) {
List<SimpleName> occurrences = new ArrayList<>();
variableNames(code).forEach((name, nodes) -> {
if (selected.contains(name)) {
occurrences.addAll(nodes);
}
});
occurrences.sort(Comparator.comparingInt(SimpleName::getStartPosition).reversed());
StringBuilder result = new StringBuilder(code);
for (SimpleName node : occurrences) {
result.insert(node.getStartPosition(), '$');
}
return result.toString();
}
private static Map<String, List<SimpleName>> variableNames(String code) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(code.toCharArray());
String trimmed = code.stripTrailing();
parser.setKind(trimmed.endsWith(";") || trimmed.endsWith("}") //$NON-NLS-1$ //$NON-NLS-2$
? ASTParser.K_STATEMENTS : ASTParser.K_EXPRESSION);
parser.setCompilerOptions(JavaCore.getOptions());
ASTNode tree = parser.createAST(null);
Map<String, List<SimpleName>> names = new TreeMap<>();
if (tree == null || (tree.getFlags() & ASTNode.MALFORMED) != 0) {
return names;
}
tree.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName node) {
String name = node.getIdentifier();
if (Character.isLowerCase(name.codePointAt(0)) && isVariablePosition(node)) {
names.computeIfAbsent(name, ignored -> new ArrayList<>()).add(node);
}
return false;
}
});
return names;
}
private static boolean isVariablePosition(SimpleName name) {
ASTNode parent = name.getParent();
if (parent instanceof Name || parent instanceof Type) {
return false;
}
if (name.isDeclaration()) {
return parent instanceof VariableDeclaration;
}
return !(parent instanceof MethodInvocation call && call.getName() == name)
&& !(parent instanceof SuperMethodInvocation superCall && superCall.getName() == name)
&& !(parent instanceof FieldAccess field && field.getName() == name)
&& !(parent instanceof SuperFieldAccess superField && superField.getName() == name)
&& !(parent instanceof ExpressionMethodReference reference && reference.getName() == name)
&& !(parent instanceof TypeMethodReference typeReference && typeReference.getName() == name)
&& !(parent instanceof SuperMethodReference superReference && superReference.getName() == name)
&& !(parent instanceof LabeledStatement label && label.getLabel() == name)
&& !(parent instanceof BreakStatement statement && statement.getLabel() == name)
&& !(parent instanceof ContinueStatement continuation && continuation.getLabel() == name);
}
}