StructuredRewriteActionContext.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.cleanup.actions;

import java.util.Objects;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;

import org.eclipse.text.edits.TextEditGroup;

import org.sandbox.jdt.triggerpattern.api.BatchTransformationProcessor.TransformationResult;
import org.sandbox.jdt.triggerpattern.api.RewriteActionValue;
import org.sandbox.jdt.triggerpattern.api.SemanticPlanValue;
import org.sandbox.jdt.triggerpattern.api.SemanticRewritePlan;
import org.sandbox.jdt.triggerpattern.api.SemanticRewritePlan.NodeKey;

/** Shared execution context and fail-closed resolver for structured actions. */
public final class StructuredRewriteActionContext {

	private final TransformationResult result;
	private final SemanticRewritePlan plan;
	private final CompilationUnitRewrite cuRewrite;
	private final TextEditGroup editGroup;

	public StructuredRewriteActionContext(TransformationResult result, SemanticRewritePlan plan,
			CompilationUnitRewrite cuRewrite, TextEditGroup editGroup) {
		this.result= Objects.requireNonNull(result);
		this.plan= Objects.requireNonNull(plan);
		this.cuRewrite= Objects.requireNonNull(cuRewrite);
		this.editGroup= Objects.requireNonNull(editGroup);
	}

	public TransformationResult result() {
		return result;
	}

	public SemanticRewritePlan plan() {
		return plan;
	}

	public CompilationUnitRewrite cuRewrite() {
		return cuRewrite;
	}

	public TextEditGroup editGroup() {
		return editGroup;
	}

	public AST ast() {
		return cuRewrite.getRoot().getAST();
	}

	/** Resolves an exact target and verifies that it participates in the plan. */
	public ASTNode resolveAuthorizedNode(RewriteActionValue value) throws CoreException {
		ASTNode node= resolveNode(value);
		NodeKey key= NodeKey.from(node);
		if (key == null) {
			throw failure("Structured action target has no stable semantic key"); //$NON-NLS-1$
		}
		boolean planned= plan.rolesByNode().containsKey(key) || plan.valuesByNode().containsKey(key)
				|| plan.relations().stream().anyMatch(relation ->
						key.equals(relation.source()) || key.equals(relation.target()));
		if (!planned) {
			throw failure("Structured action target is not present in the semantic plan: " + key); //$NON-NLS-1$
		}
		return node;
	}

	/** Resolves the primary match or one explicit pattern binding. */
	public ASTNode resolveNode(RewriteActionValue value) throws CoreException {
		if (value instanceof RewriteActionValue.MatchedNode) {
			ASTNode node= result.match().getMatchedNode();
			if (node == null) {
				throw failure("Structured action has no primary matched node"); //$NON-NLS-1$
			}
			return node;
		}
		if (value instanceof RewriteActionValue.Binding binding) {
			ASTNode node= result.match().getBinding(binding.placeholder());
			if (node == null) {
				throw failure("Missing structured action binding " + binding.placeholder()); //$NON-NLS-1$
			}
			return node;
		}
		throw failure("Structured action target must be the primary match or a pattern binding"); //$NON-NLS-1$
	}

	/** Resolves a scalar typed value, including one semantic-plan fact reference. */
	public SemanticPlanValue resolveSemanticValue(RewriteActionValue value) throws CoreException {
		if (value instanceof RewriteActionValue.Literal literal) {
			return literal.value();
		}
		if (value instanceof RewriteActionValue.PlanValue reference) {
			ASTNode target= reference.placeholder() == null
					? result.match().getMatchedNode()
					: result.match().getBinding(reference.placeholder());
			if (target == null) {
				throw failure("Missing plan-value target binding " + reference.placeholder()); //$NON-NLS-1$
			}
			return plan.value(target, reference.factName()).orElseThrow(() ->
					failure("Missing semantic-plan fact " + reference.factName())); //$NON-NLS-1$
		}
		if (value instanceof RewriteActionValue.Binding || value instanceof RewriteActionValue.MatchedNode) {
			NodeKey key= NodeKey.from(resolveAuthorizedNode(value));
			return SemanticPlanValue.node(key);
		}
		throw failure("Expected a scalar structured action value"); //$NON-NLS-1$
	}

	/**
	 * Resolves a literal/plan string or derives an identifier from one bound
	 * declaration name. The latter lets a declaration-diff compiler reuse the
	 * source placeholder instead of copying its runtime name into the DSL.
	 */
	public String resolveString(RewriteActionValue value) throws CoreException {
		if (value instanceof RewriteActionValue.Binding || value instanceof RewriteActionValue.MatchedNode) {
			ASTNode node= resolveNode(value);
			if (node instanceof SimpleName name) {
				return name.getIdentifier();
			}
			if (node instanceof SingleVariableDeclaration parameter) {
				return parameter.getName().getIdentifier();
			}
			if (node instanceof MethodDeclaration method) {
				return method.getName().getIdentifier();
			}
			if (node instanceof VariableDeclarationFragment fragment) {
				return fragment.getName().getIdentifier();
			}
			throw failure("Bound action value does not denote a Java declaration name"); //$NON-NLS-1$
		}
		SemanticPlanValue resolved= resolveSemanticValue(value);
		if (resolved instanceof SemanticPlanValue.StringValue string) {
			return string.value();
		}
		throw failure("Structured action value must resolve to a string, but was " + resolved.kind()); //$NON-NLS-1$
	}

	/** Resolves an integer value without narrowing silently. */
	public int resolveInteger(RewriteActionValue value) throws CoreException {
		SemanticPlanValue resolved= resolveSemanticValue(value);
		if (resolved instanceof SemanticPlanValue.IntegerValue integer
				&& integer.value() >= Integer.MIN_VALUE && integer.value() <= Integer.MAX_VALUE) {
			return (int) integer.value();
		}
		throw failure("Structured action value must resolve to a 32-bit integer, but was " //$NON-NLS-1$
				+ resolved.kind());
	}

	/**
	 * Creates one detached primitive, imported reference or array type.
	 *
	 * <p>Signature actions intentionally reject generic, wildcard, varargs and
	 * {@code void} source text. Richer type shapes need dedicated typed values,
	 * not arbitrary Java fragments embedded in strings.</p>
	 */
	public Type createType(String requestedType) throws CoreException {
		if (requestedType == null || requestedType.isBlank()) {
			throw failure("Structured action type must not be blank"); //$NON-NLS-1$
		}
		String typeName= requestedType.trim();
		if (typeName.indexOf('<') >= 0 || typeName.indexOf('>') >= 0
				|| typeName.indexOf('?') >= 0 || typeName.endsWith("...")) { //$NON-NLS-1$
			throw failure("Structured signature actions currently support primitive, reference and array types only: " //$NON-NLS-1$
					+ typeName);
		}
		int dimensions= 0;
		while (typeName.endsWith("[]")) { //$NON-NLS-1$
			dimensions++;
			typeName= typeName.substring(0, typeName.length() - 2).trim();
		}
		PrimitiveType.Code primitive= PrimitiveType.toCode(typeName);
		if (primitive == PrimitiveType.VOID) {
			throw failure("Structured signature actions cannot use void as a field or parameter type"); //$NON-NLS-1$
		}
		Type elementType;
		try {
			elementType= primitive == null
					? ast().newSimpleType(ast().newName(cuRewrite.getImportRewrite().addImport(typeName)))
					: ast().newPrimitiveType(primitive);
		} catch (IllegalArgumentException exception) {
			throw failure("Structured action type is not a valid Java type name: " + requestedType); //$NON-NLS-1$
		}
		return dimensions == 0 ? elementType : ast().newArrayType(elementType, dimensions);
	}

	/** Builds one detached AST expression from a typed action value. */
	public Expression createExpression(RewriteActionValue value) throws CoreException {
		if (value instanceof RewriteActionValue.ClassLiteral classLiteral) {
			String typeName= resolveString(classLiteral.typeName());
			String imported= cuRewrite.getImportRewrite().addImport(typeName);
			TypeLiteral literal= ast().newTypeLiteral();
			literal.setType(ast().newSimpleType(ast().newName(imported)));
			return literal;
		}
		if (value instanceof RewriteActionValue.Name name) {
			return ast().newName(resolveString(name.qualifiedName()));
		}
		if (value instanceof RewriteActionValue.ListValue list) {
			ArrayInitializer initializer= ast().newArrayInitializer();
			for (RewriteActionValue element : list.values()) {
				initializer.expressions().add(createExpression(element));
			}
			return initializer;
		}
		return createLiteralExpression(resolveSemanticValue(value));
	}

	@SuppressWarnings("unchecked")
	private Expression createLiteralExpression(SemanticPlanValue value) throws CoreException {
		if (value instanceof SemanticPlanValue.StringValue string) {
			StringLiteral literal= ast().newStringLiteral();
			literal.setLiteralValue(string.value());
			return literal;
		}
		if (value instanceof SemanticPlanValue.BooleanValue bool) {
			BooleanLiteral literal= ast().newBooleanLiteral(bool.value());
			return literal;
		}
		if (value instanceof SemanticPlanValue.IntegerValue integer) {
			NumberLiteral literal= ast().newNumberLiteral(Long.toString(integer.value()));
			return literal;
		}
		if (value instanceof SemanticPlanValue.ListValue list) {
			ArrayInitializer initializer= ast().newArrayInitializer();
			for (SemanticPlanValue element : list.values()) {
				initializer.expressions().add(createLiteralExpression(element));
			}
			return initializer;
		}
		throw failure("Semantic-plan node references cannot be emitted as Java expressions"); //$NON-NLS-1$
	}

	public CoreException failure(String message) {
		return failure(message, null);
	}

	private static CoreException failure(String message, Throwable cause) {
		return new CoreException(new Status(IStatus.ERROR, "sandbox_common", message, cause)); //$NON-NLS-1$
	}
}