ExpressionHelper.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.util;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Assignment;
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.InstanceofExpression;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
/**
* Utility class for common expression manipulation and analysis operations.
*
* <p>
* This class provides helper methods for manipulating and analyzing expressions
* in the context of AST transformations. It centralizes expression-related
* operations to avoid code duplication across sandbox cleanup plugins.
* </p>
*
* <p><b>Key Functionality:</b></p>
* <ul>
* <li>Expression negation with proper parenthesization</li>
* <li>Parentheses requirement detection</li>
* <li>Identity mapping detection</li>
* <li>Expression unwrapping using JDT utilities</li>
* <li>Negation detection and stripping</li>
* </ul>
*
* @see ASTNodes#getUnparenthesedExpression(Expression)
*/
public final class ExpressionHelper {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private ExpressionHelper() {
// Utility class - no instances allowed
}
/**
* Creates a negated expression with proper parenthesization.
*
* <p>
* This method is used when creating negated conditions in code transformations.
* It ensures correct operator precedence by wrapping binary and complex
* expressions in parentheses.
* </p>
*
* <p><b>Examples:</b></p>
* <pre>{@code
* // Binary expression needs parentheses
* l == null → !(l == null)
*
* // Simple name doesn't need parentheses
* flag → !flag
*
* // Method call doesn't need parentheses
* isEmpty() → !isEmpty()
* }</pre>
*
* @param ast the AST to create nodes in (must not be null)
* @param condition the condition to negate (must not be null)
* @return a negated expression with proper parenthesization
* @throws IllegalArgumentException if ast or condition is null
* @see #needsParentheses(Expression)
*/
public static Expression createNegatedExpression(AST ast, Expression condition) {
if (ast == null) {
throw new IllegalArgumentException("ast cannot be null"); //$NON-NLS-1$
}
if (condition == null) {
throw new IllegalArgumentException("condition cannot be null"); //$NON-NLS-1$
}
Expression operand = (Expression) ASTNode.copySubtree(ast, condition);
// Wrap binary expressions and other complex expressions in parentheses
// to ensure correct operator precedence
if (needsParentheses(condition)) {
ParenthesizedExpression parenthesized = ast.newParenthesizedExpression();
parenthesized.setExpression(operand);
operand = parenthesized;
}
PrefixExpression negation = ast.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand(operand);
return negation;
}
/**
* Determines if an expression needs parentheses when negated.
*
* <p>
* Returns {@code true} for expressions where the negation operator {@code !}
* would have incorrect precedence without parentheses. This includes binary
* expressions, conditional expressions, instanceof expressions, and assignments.
* </p>
*
* <p><b>Examples requiring parentheses:</b></p>
* <ul>
* <li>Binary expressions: {@code x == y}, {@code a && b}, {@code m > n}</li>
* <li>Conditional expressions: {@code condition ? a : b}</li>
* <li>Instanceof expressions: {@code obj instanceof String}</li>
* <li>Assignment expressions: {@code x = y}</li>
* </ul>
*
* <p><b>Examples NOT requiring parentheses:</b></p>
* <ul>
* <li>Simple names: {@code flag}, {@code isValid}</li>
* <li>Literals: {@code true}, {@code 42}</li>
* <li>Method calls: {@code isEmpty()}, {@code obj.check()}</li>
* <li>Field access: {@code obj.field}</li>
* </ul>
*
* @param expr the expression to check (must not be null)
* @return true if parentheses are needed when negating this expression
* @throws IllegalArgumentException if expr is null
*/
public static boolean needsParentheses(Expression expr) {
if (expr == null) {
throw new IllegalArgumentException("expr cannot be null"); //$NON-NLS-1$
}
// Binary expressions (==, !=, <, >, <=, >=, &&, ||, etc.) need parentheses
if (expr instanceof InfixExpression) {
return true;
}
// Conditional expressions (ternary operator) need parentheses
if (expr instanceof ConditionalExpression) {
return true;
}
// instanceof expressions need parentheses
if (expr instanceof InstanceofExpression) {
return true;
}
// Assignment expressions need parentheses
if (expr instanceof Assignment) {
return true;
}
// Simple names, literals, method calls, field access, etc. don't need parentheses
return false;
}
/**
* Checks if an expression represents an identity mapping.
*
* <p>
* An identity mapping is a transformation where the input is returned unchanged,
* such as {@code num -> num}. This is used to detect when a transformation can
* be skipped because it doesn't transform the input.
* </p>
*
* <p><b>Examples:</b></p>
* <pre>{@code
* // Identity mapping - returns true
* num -> num
*
* // Non-identity mapping - returns false
* num -> num * 2
* num -> num.toString()
* }</pre>
*
* @param expression the expression to check (must not be null)
* @param varName the variable name to compare against (may be null)
* @return true if the expression is just a reference to varName (identity
* mapping), false otherwise
* @throws IllegalArgumentException if expression is null
*/
public static boolean isIdentityMapping(Expression expression, String varName) {
if (expression == null) {
throw new IllegalArgumentException("expression cannot be null"); //$NON-NLS-1$
}
if (expression instanceof SimpleName && varName != null) {
SimpleName simpleName = (SimpleName) expression;
return simpleName.getIdentifier().equals(varName);
}
return false;
}
/**
* Strips the negation from a negated expression using JDT's unwrapping utility.
*
* <p>
* This method handles {@link ParenthesizedExpression} wrapping using JDT's
* {@link ASTNodes#getUnparenthesedExpression(Expression)} to ensure proper
* unwrapping of complex nested expressions.
* </p>
*
* <p><b>Examples:</b></p>
* <pre>{@code
* // Simple negation
* !condition → condition
*
* // Parenthesized negation
* ((!condition)) → condition
*
* // Not a negation (returns original)
* condition → condition
* }</pre>
*
* @param expr the expression to strip negation from (must not be null)
* @return the expression without the leading NOT operator, or the original
* expression if not negated
* @throws IllegalArgumentException if expr is null
* @see ASTNodes#getUnparenthesedExpression(Expression)
*/
public static Expression stripNegation(Expression expr) {
if (expr == null) {
throw new IllegalArgumentException("expr cannot be null"); //$NON-NLS-1$
}
// Use JDT utility to unwrap parentheses
Expression unwrapped = ASTNodes.getUnparenthesedExpression(expr);
// Check if it's a negated expression
if (unwrapped instanceof PrefixExpression) {
PrefixExpression prefixExpr = (PrefixExpression) unwrapped;
if (prefixExpr.getOperator() == PrefixExpression.Operator.NOT) {
// Return the operand without the NOT
return prefixExpr.getOperand();
}
}
// Not a negated expression, return as-is
return expr;
}
/**
* Checks if an expression is a negated expression (starts with !).
*
* <p>
* This method handles {@link ParenthesizedExpression} wrapping using JDT's
* {@link ASTNodes#getUnparenthesedExpression(Expression)} to properly detect
* negation in complex nested expressions.
* </p>
*
* <p><b>Examples returning true:</b></p>
* <pre>{@code
* // Simple negation
* !condition → true
*
* // Parenthesized negation
* ((!condition)) → true
*
* // Negated comparison
* !(a == b) → true
* }</pre>
*
* <p><b>Examples returning false:</b></p>
* <pre>{@code
* // Not negated
* condition → false
*
* // Other prefix operator
* -value → false
* }</pre>
*
* @param expr the expression to check (must not be null)
* @return true if the expression is a negation (has a leading NOT operator),
* false otherwise
* @throws IllegalArgumentException if expr is null
* @see ASTNodes#getUnparenthesedExpression(Expression)
*/
public static boolean isNegatedExpression(Expression expr) {
if (expr == null) {
throw new IllegalArgumentException("expr cannot be null"); //$NON-NLS-1$
}
// Use JDT utility to unwrap parentheses
Expression unwrapped = ASTNodes.getUnparenthesedExpression(expr);
// Check if it's a negated expression
if (unwrapped instanceof PrefixExpression) {
PrefixExpression prefixExpr = (PrefixExpression) unwrapped;
return prefixExpr.getOperator() == PrefixExpression.Operator.NOT;
}
return false;
}
/**
* Gets the unparenthesized form of an expression using JDT utilities.
*
* <p>
* This is a convenience method that delegates to
* {@link ASTNodes#getUnparenthesedExpression(Expression)} for consistency
* across the codebase.
* </p>
*
* <p><b>Examples:</b></p>
* <pre>{@code
* // Single level of parentheses
* (x) → x
*
* // Multiple levels of parentheses
* (((x + y))) → x + y
*
* // No parentheses (returns original)
* x → x
* }</pre>
*
* @param expr the expression to unwrap (may be null)
* @return the unparenthesized expression, or null if expr is null
* @see ASTNodes#getUnparenthesedExpression(Expression)
*/
public static Expression getUnparenthesized(Expression expr) {
if (expr == null) {
return null;
}
return ASTNodes.getUnparenthesedExpression(expr);
}
}