ContainerLocalRewritePlan.java

/*******************************************************************************
 * Copyright (c) 2026 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
 *******************************************************************************/
package org.sandbox.jdt.container.api;

import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * Immutable, AST-free rewrite description for one local semantic container member.
 *
 * <p>A later Eclipse-dependent resolver revalidates the binding and every expected
 * occurrence against the current AST before editing. Optional verification edits can
 * retain source uses whose corresponding declarations are changed by the same
 * aggregate multi-file plan.</p>
 */
public record ContainerLocalRewritePlan(
		String compilationUnitHandle,
		String bindingKey,
		String targetInterfaceType,
		String targetImplementationType,
		TargetContainerContract targetContract,
		List<LocalEdit> edits,
		List<ArgumentTransfer> argumentTransfers) {

	/** Compatibility constructor for plans without a cross-signature argument edge. */
	public ContainerLocalRewritePlan(
			String compilationUnitHandle,
			String bindingKey,
			String targetInterfaceType,
			String targetImplementationType,
			TargetContainerContract targetContract,
			List<LocalEdit> edits) {
		this(
				compilationUnitHandle,
				bindingKey,
				targetInterfaceType,
				targetImplementationType,
				targetContract,
				edits,
				List.of());
	}

	public ContainerLocalRewritePlan {
		compilationUnitHandle= requiredText(
				compilationUnitHandle, "compilationUnitHandle"); //$NON-NLS-1$
		bindingKey= requiredText(bindingKey, "bindingKey"); //$NON-NLS-1$
		targetInterfaceType= requiredText(targetInterfaceType, "targetInterfaceType"); //$NON-NLS-1$
		targetImplementationType= requiredText(
				targetImplementationType, "targetImplementationType"); //$NON-NLS-1$
		Objects.requireNonNull(targetContract, "targetContract"); //$NON-NLS-1$
		edits= List.copyOf(Objects.requireNonNull(edits, "edits")); //$NON-NLS-1$
		argumentTransfers= List.copyOf(
				Objects.requireNonNull(argumentTransfers, "argumentTransfers")); //$NON-NLS-1$
		validateEdits(edits, argumentTransfers);
	}

	/** One local semantic rewrite or verification anchored to source evidence. */
	public record LocalEdit(
			EditKind kind,
			int sourceStart,
			int sourceLength) {

		public LocalEdit {
			Objects.requireNonNull(kind, "kind"); //$NON-NLS-1$
			validateSourceRange(sourceStart, sourceLength);
		}
	}

	/**
	 * Exact target of one unchanged argument whose parameter declaration migrates in
	 * the same aggregate plan.
	 */
	public record ArgumentTransfer(
			String methodJavaElementHandle,
			int parameterIndex,
			int sourceStart,
			int sourceLength) {

		public ArgumentTransfer {
			methodJavaElementHandle= requiredText(
					methodJavaElementHandle, "methodJavaElementHandle"); //$NON-NLS-1$
			if (parameterIndex < 0) {
				throw new IllegalArgumentException(
						"parameterIndex must not be negative"); //$NON-NLS-1$
			}
			validateSourceRange(sourceStart, sourceLength);
		}
	}

	/** Result that preserves rejection explanations instead of returning null. */
	public record PlanningResult(
			Optional<ContainerLocalRewritePlan> plan,
			List<PlanningDiagnostic> diagnostics) {

		public PlanningResult {
			plan= Objects.requireNonNull(plan, "plan"); //$NON-NLS-1$
			diagnostics= List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); //$NON-NLS-1$
			if (plan.isPresent() == !diagnostics.isEmpty()) {
				throw new IllegalArgumentException(
						"A local rewrite result must contain either one plan or diagnostics"); //$NON-NLS-1$
			}
		}

		public static PlanningResult accepted(ContainerLocalRewritePlan plan) {
			return new PlanningResult(Optional.of(plan), List.of());
		}

		public static PlanningResult rejected(List<PlanningDiagnostic> diagnostics) {
			if (diagnostics.isEmpty()) {
				throw new IllegalArgumentException("A rejected rewrite plan requires diagnostics"); //$NON-NLS-1$
			}
			return new PlanningResult(Optional.empty(), diagnostics);
		}

		public boolean ready() {
			return plan.isPresent();
		}
	}

	/** One fail-closed eligibility explanation. */
	public record PlanningDiagnostic(
			DiagnosticKind kind,
			String message) {

		public PlanningDiagnostic {
			Objects.requireNonNull(kind, "kind"); //$NON-NLS-1$
			message= requiredText(message, "message"); //$NON-NLS-1$
		}
	}

	public enum EditKind {
		CHANGE_LOCAL_DECLARATION,
		REPLACE_EMPTY_ARRAY_INITIALIZER,
		REMOVE_ARRAY_GROWTH,
		REPLACE_TAIL_WRITE_WITH_ADD,
		REPLACE_LENGTH_WITH_SIZE,
		/** Verifies an allowed encounter-order traversal requiring no source edit. */
		VERIFY_ENCOUNTER_ITERATION,
		/** Verifies an unchanged argument whose target signature migrates atomically. */
		VERIFY_ARGUMENT_TRANSFER
	}

	public enum DiagnosticKind {
		NOT_AUTOMATIC,
		FLOW_NOT_STRICTLY_LOCAL,
		SOURCE_BINDING_MISMATCH,
		UNSUPPORTED_TARGET,
		UNSUPPORTED_ELEMENT_DOMAIN,
		POSITIONAL_SEMANTICS,
		MISSING_APPEND_PATTERN,
		UNBALANCED_APPEND_PATTERN,
		ARGUMENT_TRANSFER_MISMATCH,
		UNSUPPORTED_EVIDENCE
	}

	private static void validateEdits(
			List<LocalEdit> edits,
			List<ArgumentTransfer> argumentTransfers) {
		if (edits.isEmpty()) {
			throw new IllegalArgumentException("A local rewrite plan requires edits"); //$NON-NLS-1$
		}
		Set<EditKind> kinds= EnumSet.noneOf(EditKind.class);
		Set<SourceRange> expectedTransfers= HashSet.newHashSet(edits.size());
		for (LocalEdit edit : edits) {
			kinds.add(edit.kind());
			if (edit.kind() == EditKind.VERIFY_ARGUMENT_TRANSFER) {
				expectedTransfers.add(new SourceRange(
						edit.sourceStart(), edit.sourceLength()));
			}
		}
		if (!kinds.contains(EditKind.CHANGE_LOCAL_DECLARATION)
				|| !kinds.contains(EditKind.REPLACE_EMPTY_ARRAY_INITIALIZER)
				|| !kinds.contains(EditKind.REMOVE_ARRAY_GROWTH)
				|| !kinds.contains(EditKind.REPLACE_TAIL_WRITE_WITH_ADD)) {
			throw new IllegalArgumentException(
					"A local append-array rewrite requires declaration, initializer, growth and append edits"); //$NON-NLS-1$
		}

		Set<SourceRange> describedTransfers=
				HashSet.newHashSet(argumentTransfers.size());
		for (ArgumentTransfer transfer : argumentTransfers) {
			if (!describedTransfers.add(new SourceRange(
					transfer.sourceStart(), transfer.sourceLength()))) {
				throw new IllegalArgumentException(
						"Argument transfer source ranges must be unique"); //$NON-NLS-1$
			}
		}
		if (!expectedTransfers.equals(describedTransfers)) {
			throw new IllegalArgumentException(
					"Every argument verification edit requires one exact target description"); //$NON-NLS-1$
		}
	}

	private static void validateSourceRange(int sourceStart, int sourceLength) {
		if (sourceStart < 0 || sourceLength < 0) {
			throw new IllegalArgumentException("Source range must not be negative"); //$NON-NLS-1$
		}
	}

	private static String requiredText(String value, String fieldName) {
		String text= Objects.requireNonNull(value, fieldName).strip();
		if (text.isEmpty()) {
			throw new IllegalArgumentException(fieldName + " must not be empty"); //$NON-NLS-1$
		}
		return text;
	}

	private record SourceRange(int start, int length) {
	}
}