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.core.dom.rewrite.ImportRewrite;
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 = Pattern.of("\"\" + $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 = Pattern.of("$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 = Pattern.of("$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 = Pattern.of("$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 = Pattern.of("$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 = Pattern.of("$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 = Pattern.of("$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 = Pattern.of("$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$
}
// Pattern 9: $list.size() == 0
Pattern collectionSizePattern = Pattern.of("$list.size() == 0", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> collectionSizeMatches = ENGINE.findMatches(compilationUnit, collectionSizePattern);
for (Match match : collectionSizeMatches) {
operations.add(new CollectionIsEmptyOperation(match, "Collection size check", "$list", false)); //$NON-NLS-1$ //$NON-NLS-2$
}
// Pattern 10: $list.size() > 0
Pattern collectionNotEmptyPattern = Pattern.of("$list.size() > 0", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> collectionNotEmptyMatches = ENGINE.findMatches(compilationUnit, collectionNotEmptyPattern);
for (Match match : collectionNotEmptyMatches) {
operations.add(new CollectionIsEmptyOperation(match, "Collection non-empty check", "$list", true)); //$NON-NLS-1$ //$NON-NLS-2$
}
// Pattern 11: new StringBuilder().append($x).toString()
Pattern stringBuilderPattern = Pattern.of("new StringBuilder().append($x).toString()", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> stringBuilderMatches = ENGINE.findMatches(compilationUnit, stringBuilderPattern);
for (Match match : stringBuilderMatches) {
operations.add(new MethodToStringValueOfOperation(match, "StringBuilder single append")); //$NON-NLS-1$
}
// Pattern 12: String.format("%s", $x)
Pattern stringFormatPattern = Pattern.of("String.format(\"%s\", $x)", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> stringFormatMatches = ENGINE.findMatches(compilationUnit, stringFormatPattern);
for (Match match : stringFormatMatches) {
operations.add(new MethodToStringValueOfOperation(match, "Redundant String.format")); //$NON-NLS-1$
}
// Pattern 13: $x.toString().equals($y)
Pattern toStringEqualsPattern = Pattern.of("$x.toString().equals($y)", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> toStringEqualsMatches = ENGINE.findMatches(compilationUnit, toStringEqualsPattern);
for (Match match : toStringEqualsMatches) {
operations.add(new ObjectsEqualsOperation(match, "Null-safe toString equals")); //$NON-NLS-1$
}
// Pattern 14: $x != null ? $x : $default
Pattern nullCheckTernaryPattern = Pattern.of("$x != null ? $x : $default", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> nullCheckTernaryMatches = ENGINE.findMatches(compilationUnit, nullCheckTernaryPattern);
for (Match match : nullCheckTernaryMatches) {
operations.add(new RequireNonNullElseOperation(match, "Null-check ternary")); //$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 String.valueOf() simplification when the matched node is a MethodInvocation.
*
* <p>Used for patterns like {@code new StringBuilder().append($x).toString()}
* and {@code String.format("%s", $x)} which match MethodInvocation nodes.</p>
*/
private static class MethodToStringValueOfOperation extends CompilationUnitRewriteOperation {
private final Match match;
private final String description;
public MethodToStringValueOfOperation(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 MethodInvocation)) {
return;
}
// 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(matchedNode, 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);
}
}
/**
* Rewrite operation for collection isEmpty() simplification.
*
* <p>Handles both {@code $list.size() == 0} → {@code $list.isEmpty()}
* and {@code $list.size() > 0} → {@code !$list.isEmpty()}.</p>
*/
private static class CollectionIsEmptyOperation extends CompilationUnitRewriteOperation {
private final Match match;
private final String description;
private final String bindingName;
private final boolean negate;
public CollectionIsEmptyOperation(Match match, String description, String bindingName, boolean negate) {
this.match = match;
this.description = description;
this.bindingName = bindingName;
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();
ASTNode listNode = match.getBinding(bindingName);
if (listNode == null || !(listNode instanceof Expression)) {
return;
}
Expression listExpression = (Expression) listNode;
// Create: listExpression.isEmpty()
MethodInvocation isEmptyCall = ast.newMethodInvocation();
isEmptyCall.setExpression((Expression) ASTNode.copySubtree(ast, listExpression));
isEmptyCall.setName(ast.newSimpleName("isEmpty")); //$NON-NLS-1$
Expression replacement;
if (negate) {
// Create: !listExpression.isEmpty()
PrefixExpression negation = ast.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand(isEmptyCall);
replacement = negation;
} else {
replacement = isEmptyCall;
}
rewrite.replace(matchedNode, replacement, group);
}
}
/**
* Rewrite operation for Objects.equals() null-safe transformation.
*
* <p>Replaces {@code $x.toString().equals($y)} with {@code Objects.equals($x.toString(), $y)}.</p>
*/
private static class ObjectsEqualsOperation extends CompilationUnitRewriteOperation {
private final Match match;
private final String description;
public ObjectsEqualsOperation(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 MethodInvocation)) {
return;
}
ASTNode xNode = match.getBinding("$x"); //$NON-NLS-1$
ASTNode yNode = match.getBinding("$y"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression) ||
yNode == null || !(yNode instanceof Expression)) {
return;
}
Expression xExpression = (Expression) xNode;
Expression yExpression = (Expression) yNode;
// Add import for java.util.Objects
ImportRewrite importRewrite = cuRewrite.getImportRewrite();
String objectsType = importRewrite.addImport("java.util.Objects"); //$NON-NLS-1$
// Create: Objects.equals(x.toString(), y)
MethodInvocation equalsCall = ast.newMethodInvocation();
equalsCall.setExpression(ast.newName(objectsType));
equalsCall.setName(ast.newSimpleName("equals")); //$NON-NLS-1$
// Create x.toString()
MethodInvocation toStringCall = ast.newMethodInvocation();
toStringCall.setExpression((Expression) ASTNode.copySubtree(ast, xExpression));
toStringCall.setName(ast.newSimpleName("toString")); //$NON-NLS-1$
equalsCall.arguments().add(toStringCall);
equalsCall.arguments().add(ASTNode.copySubtree(ast, yExpression));
rewrite.replace(matchedNode, equalsCall, group);
}
}
/**
* Rewrite operation for Objects.requireNonNullElse() transformation.
*
* <p>Replaces {@code $x != null ? $x : $default} with {@code Objects.requireNonNullElse($x, $default)}.</p>
*/
private static class RequireNonNullElseOperation extends CompilationUnitRewriteOperation {
private final Match match;
private final String description;
public RequireNonNullElseOperation(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 ConditionalExpression)) {
return;
}
ASTNode xNode = match.getBinding("$x"); //$NON-NLS-1$
ASTNode defaultNode = match.getBinding("$default"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression) ||
defaultNode == null || !(defaultNode instanceof Expression)) {
return;
}
Expression xExpression = (Expression) xNode;
Expression defaultExpression = (Expression) defaultNode;
// Add import for java.util.Objects
ImportRewrite importRewrite = cuRewrite.getImportRewrite();
String objectsType = importRewrite.addImport("java.util.Objects"); //$NON-NLS-1$
// Create: Objects.requireNonNullElse(x, default)
MethodInvocation requireNonNullElseCall = ast.newMethodInvocation();
requireNonNullElseCall.setExpression(ast.newName(objectsType));
requireNonNullElseCall.setName(ast.newSimpleName("requireNonNullElse")); //$NON-NLS-1$
requireNonNullElseCall.arguments().add(ASTNode.copySubtree(ast, xExpression));
requireNonNullElseCall.arguments().add(ASTNode.copySubtree(ast, defaultExpression));
rewrite.replace(matchedNode, requireNonNullElseCall, group);
}
}
}