CodeSequenceMatcher.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.lib;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
/**
* Code Sequence Matcher - Matches AST subtrees with variable normalization
*
* This class compares code sequences to determine if they are structurally equivalent,
* even when different variable names are used. It creates a mapping between variables
* in the target and candidate sequences.
*/
public class CodeSequenceMatcher {
/**
* Try to match a candidate sequence against a target sequence
*
* @param targetSequence The target statement sequence (from the method being searched for)
* @param candidateSequence The candidate statement sequence (from inline code)
* @return VariableMapping if match successful, null otherwise
*/
public static VariableMapping matchSequence(List<Statement> targetSequence, List<Statement> candidateSequence) {
if (targetSequence == null || candidateSequence == null) {
return null;
}
if (targetSequence.size() != candidateSequence.size()) {
return null;
}
VariableMapping mapping = new VariableMapping();
// Match each statement pair
for (int i = 0; i < targetSequence.size(); i++) {
Statement targetStmt = targetSequence.get(i);
Statement candidateStmt = candidateSequence.get(i);
if (!matchStatement(targetStmt, candidateStmt, mapping)) {
return null;
}
}
return mapping;
}
/**
* Match two statements with variable mapping
*/
@SuppressWarnings("unchecked")
private static boolean matchStatement(Statement target, Statement candidate, VariableMapping mapping) {
if (target == null || candidate == null) {
return false;
}
// Special case: return statement can match variable declaration
// This allows matching code like "return x + y;" with "int result = x + y;"
if (target instanceof org.eclipse.jdt.core.dom.ReturnStatement &&
candidate instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement) {
org.eclipse.jdt.core.dom.ReturnStatement returnStmt = (org.eclipse.jdt.core.dom.ReturnStatement) target;
org.eclipse.jdt.core.dom.VariableDeclarationStatement varDecl = (org.eclipse.jdt.core.dom.VariableDeclarationStatement) candidate;
// Return statement must have an expression
if (returnStmt.getExpression() == null) {
return false;
}
// Variable declaration must have exactly one fragment with an initializer
if (varDecl.fragments().size() != 1) {
return false;
}
// Defensive check: ensure fragment list is not empty before accessing index 0
if (varDecl.fragments().isEmpty()) {
return false;
}
org.eclipse.jdt.core.dom.VariableDeclarationFragment fragment =
(org.eclipse.jdt.core.dom.VariableDeclarationFragment) varDecl.fragments().get(0);
if (fragment.getInitializer() == null) {
return false;
}
// Match the return expression against the initializer expression
VariableMappingMatcher matcher = new VariableMappingMatcher(mapping);
return returnStmt.getExpression().subtreeMatch(matcher, fragment.getInitializer());
}
// Must be same statement type for normal matching
if (target.getNodeType() != candidate.getNodeType()) {
return false;
}
// Use custom AST matcher that tracks variable mappings
VariableMappingMatcher matcher = new VariableMappingMatcher(mapping);
return target.subtreeMatch(matcher, candidate);
}
/**
* Custom AST Matcher that tracks variable name mappings
*/
private static class VariableMappingMatcher extends ASTMatcher {
private final VariableMapping mapping;
public VariableMappingMatcher(VariableMapping mapping) {
this.mapping = mapping;
}
@Override
public boolean match(SimpleName node, Object other) {
if (!(other instanceof org.eclipse.jdt.core.dom.ASTNode)) {
return false;
}
String targetName = node.getIdentifier();
// If other is also a SimpleName, do normal name mapping
if (other instanceof SimpleName) {
SimpleName otherName = (SimpleName) other;
String candidateName = otherName.getIdentifier();
return mapping.addMapping(targetName, candidateName);
}
// If other is a complex expression (not a SimpleName), store it as an expression mapping
// This handles cases like: parameter 'a' matching expression 'u.getFirst()'
if (other instanceof org.eclipse.jdt.core.dom.Expression) {
org.eclipse.jdt.core.dom.Expression candidateExpr = (org.eclipse.jdt.core.dom.Expression) other;
mapping.addExpressionMapping(targetName, candidateExpr);
return true;
}
// For all other ASTNode kinds (e.g., Statement, Type, etc.) we intentionally do not
// create a mapping and report "no match" to avoid binding a SimpleName placeholder
// to structurally larger constructs.
return false;
}
}
}