StringSimplificationHintProvider.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.triggerpattern.string;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
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.ImportRewrite;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
import org.eclipse.jdt.ui.text.java.correction.ASTRewriteCorrectionProposal;
import org.eclipse.swt.graphics.Image;
import org.sandbox.jdt.triggerpattern.api.Hint;
import org.sandbox.jdt.triggerpattern.api.HintContext;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.TriggerPattern;
/**
* Hint provider for string and boolean simplification patterns using TriggerPattern.
*
* <p>This class demonstrates using the TriggerPattern engine to suggest
* cleaner code patterns. It provides hints for:</p>
* <ul>
* <li>Empty string concatenation: {@code "" + $x} → {@code String.valueOf($x)}</li>
* <li>Redundant toString: {@code $x + ""} → {@code String.valueOf($x)}</li>
* <li>String length check: {@code $str.length() == 0} → {@code $str.isEmpty()}</li>
* <li>String equals empty: {@code $str.equals("")} → {@code $str.isEmpty()}</li>
* <li>Boolean comparison: {@code $x == true} → {@code $x}</li>
* <li>Ternary boolean return: {@code $cond ? true : false} → {@code $cond}</li>
* <li>Redundant null check: {@code $x != null && $x.isEmpty()} → use Optional or guard</li>
* <li>Collection size check: {@code $list.size() == 0} → {@code $list.isEmpty()}</li>
* <li>Negated isEmpty: {@code !$str.isEmpty() == false} → {@code !$str.isEmpty()}</li>
* <li>StringBuilder single append: {@code new StringBuilder().append($x).toString()} → {@code String.valueOf($x)}</li>
* <li>Redundant String.format: {@code String.format("%s", $x)} → {@code String.valueOf($x)}</li>
* </ul>
*
* @since 1.2.2
*/
public class StringSimplificationHintProvider {
/**
* Suggests replacing {@code "" + $x} with {@code String.valueOf($x)}.
*
* <p>Example: {@code "" + value} becomes {@code String.valueOf(value)}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "\"\" + $x", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Replace with String.valueOf()",
description = "Replaces empty string concatenation with String.valueOf() for clarity")
public static IJavaCompletionProposal replaceEmptyStringConcatenation(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return null;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the bound variable from placeholders
ASTNode xNode = ctx.getMatch().getBinding("$x"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression)) {
return null;
}
Expression valueExpression = (Expression) xNode;
// Create the replacement: String.valueOf(valueExpression)
AST ast = ctx.getASTRewrite().getAST();
ImportRewrite importRewrite = ctx.getImportRewrite();
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
ctx.getASTRewrite().replace(infixExpr, methodInvocation, null);
// Create the proposal
String label = "Replace '" + infixExpr + "' with 'String.valueOf(...)'"; //$NON-NLS-1$ //$NON-NLS-2$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $x + ""} with {@code String.valueOf($x)}.
*
* <p>Example: {@code value + ""} becomes {@code String.valueOf(value)}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$x + \"\"", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Replace with String.valueOf()",
description = "Replaces concatenation with empty string with String.valueOf() for clarity")
public static IJavaCompletionProposal replaceTrailingEmptyString(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return null;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the bound variable
ASTNode xNode = ctx.getMatch().getBinding("$x"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression)) {
return null;
}
Expression valueExpression = (Expression) xNode;
// Create the replacement: String.valueOf(valueExpression)
AST ast = ctx.getASTRewrite().getAST();
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
ctx.getASTRewrite().replace(infixExpr, methodInvocation, null);
// Create the proposal
String label = "Replace '" + infixExpr + "' with 'String.valueOf(...)'"; //$NON-NLS-1$ //$NON-NLS-2$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $str.length() == 0} with {@code $str.isEmpty()}.
*
* <p>Example: {@code str.length() == 0} becomes {@code str.isEmpty()}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$str.length() == 0", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Use isEmpty()",
description = "Replaces length() == 0 check with isEmpty() for better readability")
public static IJavaCompletionProposal replaceStringLengthCheck(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return null;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the string expression
ASTNode strNode = ctx.getMatch().getBinding("$str"); //$NON-NLS-1$
if (strNode == null || !(strNode instanceof Expression)) {
return null;
}
Expression strExpression = (Expression) strNode;
// Create the replacement: strExpression.isEmpty()
AST ast = ctx.getASTRewrite().getAST();
MethodInvocation isEmptyCall = ast.newMethodInvocation();
isEmptyCall.setExpression((Expression) ASTNode.copySubtree(ast, strExpression));
isEmptyCall.setName(ast.newSimpleName("isEmpty")); //$NON-NLS-1$
// Apply the rewrite
ctx.getASTRewrite().replace(infixExpr, isEmptyCall, null);
// Create the proposal
String label = "Replace '" + infixExpr + "' with 'isEmpty()'"; //$NON-NLS-1$ //$NON-NLS-2$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $str.equals("")} with {@code $str.isEmpty()}.
*
* <p>Example: {@code str.equals("")} becomes {@code str.isEmpty()}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$str.equals(\"\")", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Use isEmpty()",
description = "Replaces equals(\"\") check with isEmpty() for better performance")
public static IJavaCompletionProposal replaceEqualsEmptyString(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof MethodInvocation)) {
return null;
}
MethodInvocation methodInvocation = (MethodInvocation) matchedNode;
// Get the string expression
ASTNode strNode = ctx.getMatch().getBinding("$str"); //$NON-NLS-1$
if (strNode == null || !(strNode instanceof Expression)) {
return null;
}
Expression strExpression = (Expression) strNode;
// Create the replacement: strExpression.isEmpty()
AST ast = ctx.getASTRewrite().getAST();
MethodInvocation isEmptyCall = ast.newMethodInvocation();
isEmptyCall.setExpression((Expression) ASTNode.copySubtree(ast, strExpression));
isEmptyCall.setName(ast.newSimpleName("isEmpty")); //$NON-NLS-1$
// Apply the rewrite
ctx.getASTRewrite().replace(methodInvocation, isEmptyCall, null);
// Create the proposal
String label = "Replace '" + methodInvocation + "' with 'isEmpty()'"; //$NON-NLS-1$ //$NON-NLS-2$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $x == true} with {@code $x}.
*
* <p>Example: {@code flag == true} becomes {@code flag}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$x == true", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Simplify boolean comparison",
description = "Removes redundant comparison with true")
public static IJavaCompletionProposal simplifyBooleanComparisonTrue(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return null;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the boolean variable
ASTNode xNode = ctx.getMatch().getBinding("$x"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression)) {
return null;
}
Expression boolExpression = (Expression) xNode;
// Create the replacement: just the boolean expression
AST ast = ctx.getASTRewrite().getAST();
Expression replacement = (Expression) ASTNode.copySubtree(ast, boolExpression);
// Apply the rewrite
ctx.getASTRewrite().replace(infixExpr, replacement, null);
// Create the proposal
String label = "Simplify '" + infixExpr + "' to '" + boolExpression + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $x == false} with {@code !$x}.
*
* <p>Example: {@code flag == false} becomes {@code !flag}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$x == false", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Simplify boolean comparison",
description = "Replaces comparison with false with negation operator")
public static IJavaCompletionProposal simplifyBooleanComparisonFalse(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return null;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the boolean variable
ASTNode xNode = ctx.getMatch().getBinding("$x"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression)) {
return null;
}
Expression boolExpression = (Expression) xNode;
// Create the replacement: !boolExpression
AST ast = ctx.getASTRewrite().getAST();
PrefixExpression negation = ast.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand((Expression) ASTNode.copySubtree(ast, boolExpression));
// Apply the rewrite
ctx.getASTRewrite().replace(infixExpr, negation, null);
// Create the proposal
String label = "Simplify '" + infixExpr + "' to '!" + boolExpression + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $cond ? true : false} with {@code $cond}.
*
* <p>Example: {@code isValid() ? true : false} becomes {@code isValid()}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$cond ? true : false", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Simplify ternary boolean",
description = "Replaces redundant ternary expression with condition itself")
public static IJavaCompletionProposal simplifyTernaryBooleanTrueFalse(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof ConditionalExpression)) {
return null;
}
ConditionalExpression ternary = (ConditionalExpression) matchedNode;
// Get the condition
ASTNode condNode = ctx.getMatch().getBinding("$cond"); //$NON-NLS-1$
if (condNode == null || !(condNode instanceof Expression)) {
return null;
}
Expression condition = (Expression) condNode;
// Create the replacement: just the condition
AST ast = ctx.getASTRewrite().getAST();
Expression replacement = (Expression) ASTNode.copySubtree(ast, condition);
// Apply the rewrite
ctx.getASTRewrite().replace(ternary, replacement, null);
// Create the proposal
String label = "Simplify '" + ternary + "' to '" + condition + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $cond ? false : true} with {@code !$cond}.
*
* <p>Example: {@code isValid() ? false : true} becomes {@code !isValid()}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$cond ? false : true", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Simplify ternary boolean",
description = "Replaces inverted ternary expression with negated condition")
public static IJavaCompletionProposal simplifyTernaryBooleanFalseTrue(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof ConditionalExpression)) {
return null;
}
ConditionalExpression ternary = (ConditionalExpression) matchedNode;
// Get the condition
ASTNode condNode = ctx.getMatch().getBinding("$cond"); //$NON-NLS-1$
if (condNode == null || !(condNode instanceof Expression)) {
return null;
}
Expression condition = (Expression) condNode;
// Create the replacement: !condition
AST ast = ctx.getASTRewrite().getAST();
PrefixExpression negation = ast.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand((Expression) ASTNode.copySubtree(ast, condition));
// Apply the rewrite
ctx.getASTRewrite().replace(ternary, negation, null);
// Create the proposal
String label = "Simplify '" + ternary + "' to '!" + condition + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $list.size() == 0} with {@code $list.isEmpty()}.
*
* <p>This pattern works for any Collection type (List, Set, Map, etc.)</p>
* <p>Example: {@code myList.size() == 0} becomes {@code myList.isEmpty()}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$list.size() == 0", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Use isEmpty() for collections",
description = "Replaces size() == 0 with isEmpty() for better readability and performance")
public static IJavaCompletionProposal replaceCollectionSizeCheck(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return null;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the collection expression
ASTNode listNode = ctx.getMatch().getBinding("$list"); //$NON-NLS-1$
if (listNode == null || !(listNode instanceof Expression)) {
return null;
}
Expression listExpression = (Expression) listNode;
// Create the replacement: listExpression.isEmpty()
AST ast = ctx.getASTRewrite().getAST();
MethodInvocation isEmptyCall = ast.newMethodInvocation();
isEmptyCall.setExpression((Expression) ASTNode.copySubtree(ast, listExpression));
isEmptyCall.setName(ast.newSimpleName("isEmpty")); //$NON-NLS-1$
// Apply the rewrite
ctx.getASTRewrite().replace(infixExpr, isEmptyCall, null);
// Create the proposal
String label = "Replace '" + infixExpr + "' with 'isEmpty()'"; //$NON-NLS-1$ //$NON-NLS-2$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $list.size() > 0} with {@code !$list.isEmpty()}.
*
* <p>Example: {@code myList.size() > 0} becomes {@code !myList.isEmpty()}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$list.size() > 0", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Use !isEmpty() for collections",
description = "Replaces size() > 0 with !isEmpty() for better readability")
public static IJavaCompletionProposal replaceCollectionSizeGreaterThanZero(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return null;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the collection expression
ASTNode listNode = ctx.getMatch().getBinding("$list"); //$NON-NLS-1$
if (listNode == null || !(listNode instanceof Expression)) {
return null;
}
Expression listExpression = (Expression) listNode;
// Create the replacement: !listExpression.isEmpty()
AST ast = ctx.getASTRewrite().getAST();
MethodInvocation isEmptyCall = ast.newMethodInvocation();
isEmptyCall.setExpression((Expression) ASTNode.copySubtree(ast, listExpression));
isEmptyCall.setName(ast.newSimpleName("isEmpty")); //$NON-NLS-1$
PrefixExpression negation = ast.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand(isEmptyCall);
// Apply the rewrite
ctx.getASTRewrite().replace(infixExpr, negation, null);
// Create the proposal
String label = "Replace '" + infixExpr + "' with '!isEmpty()'"; //$NON-NLS-1$ //$NON-NLS-2$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code new StringBuilder().append($x).toString()} with {@code String.valueOf($x)}.
*
* <p>This is a more complex pattern that demonstrates TriggerPattern's ability to match
* chained method calls. The pattern identifies an anti-pattern where StringBuilder is
* unnecessarily used for a single value conversion.</p>
*
* <p>Example: {@code new StringBuilder().append(value).toString()} becomes {@code String.valueOf(value)}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "new StringBuilder().append($x).toString()", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Simplify StringBuilder single append",
description = "Replaces unnecessary StringBuilder with single append with String.valueOf()")
public static IJavaCompletionProposal simplifyStringBuilderSingleAppend(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof MethodInvocation)) {
return null;
}
MethodInvocation methodInvocation = (MethodInvocation) matchedNode;
// Get the appended value
ASTNode xNode = ctx.getMatch().getBinding("$x"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression)) {
return null;
}
Expression valueExpression = (Expression) xNode;
// Create the replacement: String.valueOf(valueExpression)
AST ast = ctx.getASTRewrite().getAST();
MethodInvocation valueOfCall = ast.newMethodInvocation();
valueOfCall.setExpression(ast.newName("String")); //$NON-NLS-1$
valueOfCall.setName(ast.newSimpleName("valueOf")); //$NON-NLS-1$
valueOfCall.arguments().add(ASTNode.copySubtree(ast, valueExpression));
// Apply the rewrite
ctx.getASTRewrite().replace(methodInvocation, valueOfCall, null);
// Create the proposal
String label = "Replace StringBuilder single append with 'String.valueOf(...)'"; //$NON-NLS-1$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code String.format("%s", $x)} with {@code String.valueOf($x)}.
*
* <p>This pattern identifies cases where String.format is used with a simple "%s" format
* string, which is unnecessarily complex compared to String.valueOf().</p>
*
* <p>Example: {@code String.format("%s", obj)} becomes {@code String.valueOf(obj)}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "String.format(\"%s\", $x)", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Simplify String.format",
description = "Replaces String.format with simple %s with String.valueOf() for better performance")
public static IJavaCompletionProposal simplifyStringFormat(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof MethodInvocation)) {
return null;
}
MethodInvocation methodInvocation = (MethodInvocation) matchedNode;
// Get the formatted value
ASTNode xNode = ctx.getMatch().getBinding("$x"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression)) {
return null;
}
Expression valueExpression = (Expression) xNode;
// Create the replacement: String.valueOf(valueExpression)
AST ast = ctx.getASTRewrite().getAST();
MethodInvocation valueOfCall = ast.newMethodInvocation();
valueOfCall.setExpression(ast.newName("String")); //$NON-NLS-1$
valueOfCall.setName(ast.newSimpleName("valueOf")); //$NON-NLS-1$
valueOfCall.arguments().add(ASTNode.copySubtree(ast, valueExpression));
// Apply the rewrite
ctx.getASTRewrite().replace(methodInvocation, valueOfCall, null);
// Create the proposal
String label = "Replace 'String.format(\"%s\", ...)' with 'String.valueOf(...)'"; //$NON-NLS-1$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $x.toString().equals($y)} with {@code Objects.equals($x.toString(), $y)}.
*
* <p>This is a complex pattern demonstrating null-safety improvements. The original code
* will throw NPE if $x is null, while the suggested replacement handles null safely.</p>
*
* <p>Example: {@code obj.toString().equals(str)} becomes {@code Objects.equals(obj.toString(), str)}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$x.toString().equals($y)", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Use Objects.equals for null safety",
description = "Replaces potential NPE-prone equals() with null-safe Objects.equals()")
public static IJavaCompletionProposal useObjectsEquals(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof MethodInvocation)) {
return null;
}
MethodInvocation methodInvocation = (MethodInvocation) matchedNode;
// Get both expressions
ASTNode xNode = ctx.getMatch().getBinding("$x"); //$NON-NLS-1$
ASTNode yNode = ctx.getMatch().getBinding("$y"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression) ||
yNode == null || !(yNode instanceof Expression)) {
return null;
}
Expression xExpression = (Expression) xNode;
Expression yExpression = (Expression) yNode;
// Create the replacement: Objects.equals(x.toString(), y)
AST ast = ctx.getASTRewrite().getAST();
ImportRewrite importRewrite = ctx.getImportRewrite();
// Add import for Objects if needed
String objectsType = importRewrite.addImport("java.util.Objects"); //$NON-NLS-1$
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));
// Apply the rewrite
ctx.getASTRewrite().replace(methodInvocation, equalsCall, null);
// Create the proposal
String label = "Replace with null-safe 'Objects.equals(...)'"; //$NON-NLS-1$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
/**
* Suggests replacing {@code $x != null ? $x : $default} with {@code Objects.requireNonNullElse($x, $default)}.
*
* <p>This is a complex pattern that demonstrates modern Java API usage (Java 9+).
* It shows how TriggerPattern can suggest more idiomatic code that leverages newer APIs.</p>
*
* <p>Example: {@code obj != null ? obj : "default"} becomes {@code Objects.requireNonNullElse(obj, "default")}</p>
*
* @param ctx the hint context containing the match and AST information
* @return a completion proposal, or null if the pattern doesn't match
*/
@TriggerPattern(value = "$x != null ? $x : $default", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Use Objects.requireNonNullElse",
description = "Replaces null-check ternary with Objects.requireNonNullElse() (Java 9+)")
public static IJavaCompletionProposal useRequireNonNullElse(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof ConditionalExpression)) {
return null;
}
ConditionalExpression ternary = (ConditionalExpression) matchedNode;
// Get the expressions
ASTNode xNode = ctx.getMatch().getBinding("$x"); //$NON-NLS-1$
ASTNode defaultNode = ctx.getMatch().getBinding("$default"); //$NON-NLS-1$
if (xNode == null || !(xNode instanceof Expression) ||
defaultNode == null || !(defaultNode instanceof Expression)) {
return null;
}
Expression xExpression = (Expression) xNode;
Expression defaultExpression = (Expression) defaultNode;
// Create the replacement: Objects.requireNonNullElse(x, default)
AST ast = ctx.getASTRewrite().getAST();
ImportRewrite importRewrite = ctx.getImportRewrite();
// Add import for Objects if needed
String objectsType = importRewrite.addImport("java.util.Objects"); //$NON-NLS-1$
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));
// Apply the rewrite
ctx.getASTRewrite().replace(ternary, requireNonNullElseCall, null);
// Create the proposal
String label = "Replace with 'Objects.requireNonNullElse(...)'"; //$NON-NLS-1$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10, // relevance
(Image) null
);
return proposal;
}
}