SideEffectAnalysis.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer and others.
 *
 * 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
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.internal;

import java.lang.reflect.Modifier;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ArrayAccess;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.CharacterLiteral;
import org.eclipse.jdt.core.dom.ConditionalExpression;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.InstanceofExpression;
import org.eclipse.jdt.core.dom.NullLiteral;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PatternInstanceofExpression;
import org.eclipse.jdt.core.dom.PostfixExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.SuperFieldAccess;
import org.eclipse.jdt.core.dom.TextBlock;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.TypePattern;
import org.sandbox.jdt.triggerpattern.api.GuardContext;

/**
 * A deliberately limited expression-effects proof, shared by guard evaluation.
 * It does not prove that reads are stable, non-throwing or safe to move.
 * Type cases stay separate because JDT binding recovery can crash on grouped
 * patterns when their types are unresolved.
 */
final class SideEffectAnalysis {

	private SideEffectAnalysis() {
	}

	/**
	 * Proves a non-throwing, stable value read only. This says nothing about the
	 * mutability of the object it denotes or conversions introduced by a rewrite.
	 */
	static boolean isStableValue(Expression expression, GuardContext context) {
		if ((expression.getFlags() & (ASTNode.MALFORMED | ASTNode.RECOVERED)) != 0) {
			context.markUnknown("isStableValue", "Recovered or malformed expression"); //$NON-NLS-1$ //$NON-NLS-2$
			return false;
		}
		return switch (expression) {
		case BooleanLiteral _ -> true;
		case CharacterLiteral _ -> true;
		case NullLiteral _ -> true;
		case NumberLiteral _ -> true;
		case StringLiteral _ -> true;
		case TextBlock _ -> true;
		case ParenthesizedExpression parenthesized -> isStableValue(parenthesized.getExpression(), context);
		case SimpleName name -> {
			IBinding binding= name.resolveBinding();
			if (binding == null || binding.isRecovered()
					|| binding instanceof IVariableBinding variable
						&& (variable.getType() == null || variable.getType().isRecovered())) {
				context.markUnknown("isStableValue", "Variable binding unavailable: " + name); //$NON-NLS-1$ //$NON-NLS-2$
				yield false;
			}
			yield binding instanceof IVariableBinding variable && !variable.isField()
					&& Modifier.isFinal(variable.getModifiers());
		}
		default -> false;
		};
	}

	static boolean hasNoSideEffect(Expression expression, GuardContext context) {
		if ((expression.getFlags() & (ASTNode.MALFORMED | ASTNode.RECOVERED)) != 0) {
			return unknown(context, expression);
		}
		return switch (expression) {
		case BooleanLiteral _ -> true;
		case CharacterLiteral _ -> true;
		case NullLiteral _ -> true;
		case NumberLiteral _ -> true;
		case StringLiteral _ -> true;
		case TextBlock _ -> true;
		case ThisExpression _ -> true;
		case TypeLiteral _ -> true;
		case SimpleName name -> safeRead(name.resolveBinding(), context, name);
		case QualifiedName name -> safeRead(name.resolveBinding(), context, name)
				&& hasNoSideEffect(name.getQualifier(), context);
		case FieldAccess field -> safeRead(field.resolveFieldBinding(), context, field)
				&& hasNoSideEffect(field.getExpression(), context);
		case SuperFieldAccess field -> safeRead(field.resolveFieldBinding(), context, field);
		case ParenthesizedExpression parenthesized -> hasNoSideEffect(parenthesized.getExpression(), context);
		case CastExpression cast -> hasNoSideEffect(cast.getExpression(), context);
		case ArrayAccess access -> hasNoSideEffect(access.getArray(), context)
				&& hasNoSideEffect(access.getIndex(), context);
		case PrefixExpression prefix -> prefix.getOperator() != PrefixExpression.Operator.INCREMENT
				&& prefix.getOperator() != PrefixExpression.Operator.DECREMENT
				&& hasNoSideEffect(prefix.getOperand(), context);
		case InfixExpression infix -> safeInfix(infix, context);
		case ConditionalExpression conditional -> hasNoSideEffect(conditional.getExpression(), context)
				&& hasNoSideEffect(conditional.getThenExpression(), context)
				&& hasNoSideEffect(conditional.getElseExpression(), context);
		case InstanceofExpression instanceOf -> hasNoSideEffect(instanceOf.getLeftOperand(), context);
		case PatternInstanceofExpression instanceOf -> {
			boolean typePattern;
			if (instanceOf.getAST().apiLevel() >= AST.JLS21) {
				typePattern= instanceOf.getPattern() instanceof TypePattern;
			} else {
				// Older DOMs expose record patterns as synthetic, unbound declarations.
				IVariableBinding variable= instanceOf.getRightOperand().resolveBinding();
				typePattern= variable != null && !variable.isRecovered();
			}
			if (!typePattern) {
				// Record deconstruction invokes accessors, including in nested patterns.
				yield unknown(context, instanceOf);
			}
			yield hasNoSideEffect(instanceOf.getLeftOperand(), context);
		}
		case Assignment _ -> false;
		case PostfixExpression _ -> false;
		// Calls (including constructors), writes, allocation and unsupported syntax
		// never become proof merely because their outer node is not a method call.
		// Unknown purity must remain unknown under guard negation as well.
		default -> unknown(context, expression);
		};
	}

	private static boolean safeRead(IBinding binding, GuardContext context, ASTNode node) {
		if (binding == null || binding.isRecovered()) {
			return unknown(context, node);
		}
		if (binding instanceof IVariableBinding variable) {
			ITypeBinding type= variable.getType();
			if (type == null || type.isRecovered()) {
				return unknown(context, node);
			}
			if (Modifier.isVolatile(variable.getModifiers())) {
				return false;
			}
			if (Modifier.isStatic(variable.getModifiers()) && variable.getConstantValue() == null) {
				return unknown(context, node);
			}
			return true;
		}
		// Qualified static names also traverse their package/type qualifier. Merely
		// naming that qualifier does not initialize a class.
		return binding.getKind() == IBinding.TYPE || binding.getKind() == IBinding.PACKAGE;
	}

	private static boolean safeInfix(InfixExpression expression, GuardContext context) {
		if (!safeOperand(expression.getLeftOperand(), expression.getOperator(), context)
				|| !safeOperand(expression.getRightOperand(), expression.getOperator(), context)) {
			return false;
		}
		for (Object operand : expression.extendedOperands()) {
			if (!safeOperand((Expression) operand, expression.getOperator(), context)) {
				return false;
			}
		}
		return true;
	}

	private static boolean safeOperand(Expression operand, InfixExpression.Operator operator,
			GuardContext context) {
		if (!hasNoSideEffect(operand, context)) {
			return false;
		}
		if (operator != InfixExpression.Operator.PLUS) {
			return true;
		}
		// String concatenation can invoke an arbitrary Object.toString(), even
		// without an explicit invocation node in the source.
		if (operand instanceof NullLiteral || operand instanceof NumberLiteral
				|| operand instanceof BooleanLiteral || operand instanceof CharacterLiteral
				|| operand instanceof StringLiteral || operand instanceof TextBlock) {
			return true;
		}
		ITypeBinding type= operand.resolveTypeBinding();
		if (type == null || type.isRecovered()) {
			return unknown(context, operand);
		}
		return type.isPrimitive() || "java.lang.String".equals(type.getQualifiedName()) //$NON-NLS-1$
				|| unknown(context, operand);
	}

	private static boolean unknown(GuardContext context, ASTNode node) {
		context.markUnknown("hasNoSideEffect", "Effects could not be proven absent: " + node); //$NON-NLS-1$ //$NON-NLS-2$
		return false;
	}
}