StringSimplificationFixCore.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 - initial API and implementation
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix;
import java.util.List;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConditionalExpression;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.triggerpattern.api.Match;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.TriggerPatternEngine;
/**
* Fix core for string simplification using TriggerPattern hints.
*
* <p>This class applies string simplification patterns as cleanup operations,
* transforming patterns like {@code "" + x} and {@code x + ""} to {@code String.valueOf(x)}.</p>
*
* @since 1.2.2
*/
public class StringSimplificationFixCore {
private static final TriggerPatternEngine ENGINE = new TriggerPatternEngine();
/**
* Finds string simplification operations in the compilation unit.
*
* @param compilationUnit the compilation unit to search
* @param operations the set to add found operations to
*/
public static void findOperations(CompilationUnit compilationUnit,
java.util.Set<CompilationUnitRewriteOperation> operations) {
// Pattern 1: "" + $x
Pattern emptyPrefixPattern = new Pattern("\"\" + $x", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> emptyPrefixMatches = ENGINE.findMatches(compilationUnit, emptyPrefixPattern);
for (Match match : emptyPrefixMatches) {
operations.add(new StringValueOfOperation(match, "Empty string prefix")); //$NON-NLS-1$
}
// Pattern 2: $x + ""
Pattern emptySuffixPattern = new Pattern("$x + \"\"", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> emptySuffixMatches = ENGINE.findMatches(compilationUnit, emptySuffixPattern);
for (Match match : emptySuffixMatches) {
operations.add(new StringValueOfOperation(match, "Empty string suffix")); //$NON-NLS-1$
}
// Pattern 3: $str.length() == 0
Pattern lengthCheckPattern = new Pattern("$str.length() == 0", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> lengthCheckMatches = ENGINE.findMatches(compilationUnit, lengthCheckPattern);
for (Match match : lengthCheckMatches) {
operations.add(new IsEmptyOperation(match, "String length check")); //$NON-NLS-1$
}
// Pattern 4: $str.equals("")
Pattern equalsEmptyPattern = new Pattern("$str.equals(\"\")", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> equalsEmptyMatches = ENGINE.findMatches(compilationUnit, equalsEmptyPattern);
for (Match match : equalsEmptyMatches) {
operations.add(new IsEmptyOperation(match, "String equals empty")); //$NON-NLS-1$
}
// Pattern 5: $x == true
Pattern boolTruePattern = new Pattern("$x == true", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> boolTrueMatches = ENGINE.findMatches(compilationUnit, boolTruePattern);
for (Match match : boolTrueMatches) {
operations.add(new SimplifyBooleanOperation(match, "Boolean == true", false)); //$NON-NLS-1$
}
// Pattern 6: $x == false
Pattern boolFalsePattern = new Pattern("$x == false", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> boolFalseMatches = ENGINE.findMatches(compilationUnit, boolFalsePattern);
for (Match match : boolFalseMatches) {
operations.add(new SimplifyBooleanOperation(match, "Boolean == false", true)); //$NON-NLS-1$
}
// Pattern 7: $cond ? true : false
Pattern ternaryTrueFalsePattern = new Pattern("$cond ? true : false", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> ternaryTrueFalseMatches = ENGINE.findMatches(compilationUnit, ternaryTrueFalsePattern);
for (Match match : ternaryTrueFalseMatches) {
operations.add(new SimplifyTernaryOperation(match, "Ternary true:false", false)); //$NON-NLS-1$
}
// Pattern 8: $cond ? false : true
Pattern ternaryFalseTruePattern = new Pattern("$cond ? false : true", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> ternaryFalseTrueMatches = ENGINE.findMatches(compilationUnit, ternaryFalseTruePattern);
for (Match match : ternaryFalseTrueMatches) {
operations.add(new SimplifyTernaryOperation(match, "Ternary false:true", true)); //$NON-NLS-1$
}
}
/**
* Rewrite operation for String.valueOf() simplification.
*/
private static class StringValueOfOperation extends CompilationUnitRewriteOperation {
private final Match match;
private final String description;
public StringValueOfOperation(Match match, String description) {
this.match = match;
this.description = description;
}
@Override
public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) {
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getRoot().getAST();
TextEditGroup group = createTextEditGroup(description, cuRewrite);
ASTNode matchedNode = match.getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the bound variable from placeholders
ASTNode xNode = match.getBinding("$x"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression)) {
return;
}
Expression valueExpression = (Expression) xNode;
// Create the replacement: String.valueOf(valueExpression)
MethodInvocation methodInvocation = ast.newMethodInvocation();
methodInvocation.setExpression(ast.newName("String")); //$NON-NLS-1$
methodInvocation.setName(ast.newSimpleName("valueOf")); //$NON-NLS-1$
methodInvocation.arguments().add(ASTNode.copySubtree(ast, valueExpression));
// Apply the rewrite
rewrite.replace(infixExpr, methodInvocation, group);
}
}
/**
* Rewrite operation for isEmpty() simplification.
*/
private static class IsEmptyOperation extends CompilationUnitRewriteOperation {
private final Match match;
private final String description;
public IsEmptyOperation(Match match, String description) {
this.match = match;
this.description = description;
}
@Override
public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) {
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getRoot().getAST();
TextEditGroup group = createTextEditGroup(description, cuRewrite);
ASTNode matchedNode = match.getMatchedNode();
// Get the bound variable from placeholders
ASTNode strNode = match.getBinding("$str"); //$NON-NLS-1$
if (strNode == null || !(strNode instanceof Expression)) {
return;
}
Expression strExpression = (Expression) strNode;
// Create the replacement: strExpression.isEmpty()
MethodInvocation isEmptyCall = ast.newMethodInvocation();
isEmptyCall.setExpression((Expression) ASTNode.copySubtree(ast, strExpression));
isEmptyCall.setName(ast.newSimpleName("isEmpty")); //$NON-NLS-1$
// Apply the rewrite
rewrite.replace(matchedNode, isEmptyCall, group);
}
}
/**
* Rewrite operation for boolean simplification.
*/
private static class SimplifyBooleanOperation extends CompilationUnitRewriteOperation {
private final Match match;
private final String description;
private final boolean negate;
public SimplifyBooleanOperation(Match match, String description, boolean negate) {
this.match = match;
this.description = description;
this.negate = negate;
}
@Override
public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) {
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getRoot().getAST();
TextEditGroup group = createTextEditGroup(description, cuRewrite);
ASTNode matchedNode = match.getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the boolean variable
ASTNode xNode = match.getBinding("$x"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression)) {
return;
}
Expression boolExpression = (Expression) xNode;
Expression replacement;
if (negate) {
// Create: !boolExpression
PrefixExpression negation = ast.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand((Expression) ASTNode.copySubtree(ast, boolExpression));
replacement = negation;
} else {
// Just use the boolean expression as-is
replacement = (Expression) ASTNode.copySubtree(ast, boolExpression);
}
// Apply the rewrite
rewrite.replace(infixExpr, replacement, group);
}
}
/**
* Rewrite operation for ternary boolean simplification.
*/
private static class SimplifyTernaryOperation extends CompilationUnitRewriteOperation {
private final Match match;
private final String description;
private final boolean negate;
public SimplifyTernaryOperation(Match match, String description, boolean negate) {
this.match = match;
this.description = description;
this.negate = negate;
}
@Override
public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) {
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getRoot().getAST();
TextEditGroup group = createTextEditGroup(description, cuRewrite);
ASTNode matchedNode = match.getMatchedNode();
if (!(matchedNode instanceof ConditionalExpression)) {
return;
}
ConditionalExpression ternary = (ConditionalExpression) matchedNode;
// Get the condition
ASTNode condNode = match.getBinding("$cond"); //$NON-NLS-1$
if (condNode == null || !(condNode instanceof Expression)) {
return;
}
Expression condition = (Expression) condNode;
Expression replacement;
if (negate) {
// Create: !condition
PrefixExpression negation = ast.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand((Expression) ASTNode.copySubtree(ast, condition));
replacement = negation;
} else {
// Just use the condition as-is
replacement = (Expression) ASTNode.copySubtree(ast, condition);
}
// Apply the rewrite
rewrite.replace(ternary, replacement, group);
}
}
}