HintPlanRequirement.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.api;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Reads the fail-closed {@code <!requires-plan: ...>} contract from a hint
 * program without otherwise interpreting the DSL.
 *
 * <p>The directive deliberately remains separate from ordinary hint metadata:
 * callers that execute a trusted plan-aware program must opt into validating
 * it before handing the program to the normal hint backend. Duplicate identical
 * declarations are tolerated, while missing values, malformed declarations and
 * conflicting declarations are rejected. Commented declarations are ignored.</p>
 *
 * <p>A semantic-plan dependency implicitly requires complete semantic bindings.
 * No second binding-policy directive is necessary: the plan-aware execution
 * boundary re-identifies every target by a stable semantic key and fails closed
 * when that is not possible.</p>
 */
public final class HintPlanRequirement {

	private static final String PREFIX= "<!requires-plan"; //$NON-NLS-1$
	private static final Pattern DIRECTIVE= Pattern.compile(
			"^\\s*<!requires-plan\\s*:\\s*([^>\\r\\n]*)>\\s*$"); //$NON-NLS-1$

	private HintPlanRequirement() {
	}

	/**
	 * Returns the declared semantic-plan contract identifier.
	 *
	 * @param content complete hint program text
	 * @return the required plan identifier, or empty for an ordinary hint program
	 * @throws IllegalArgumentException for blank, malformed or conflicting declarations
	 */
	public static Optional<String> fromContent(String content) {
		if (content == null || content.isBlank()) {
			return Optional.empty();
		}
		String requirement= null;
		boolean inBlockComment= false;
		for (String line : content.split("\\R", -1)) { //$NON-NLS-1$
			VisibleLine visibleLine= visibleLine(line, inBlockComment);
			inBlockComment= visibleLine.inBlockComment();
			String visible= visibleLine.text().trim();
			if (!visible.startsWith(PREFIX)) {
				continue;
			}
			Matcher matcher= DIRECTIVE.matcher(visible);
			if (!matcher.matches()) {
				throw new IllegalArgumentException("Malformed requires-plan declaration: " + visible); //$NON-NLS-1$
			}
			String candidate= matcher.group(1).trim();
			if (candidate.isEmpty()) {
				throw new IllegalArgumentException("requires-plan must name a semantic plan contract"); //$NON-NLS-1$
			}
			if (requirement != null && !requirement.equals(candidate)) {
				throw new IllegalArgumentException(
						"Conflicting requires-plan declarations: " + requirement + " and " + candidate); //$NON-NLS-1$ //$NON-NLS-2$
			}
			requirement= candidate;
		}
		return Optional.ofNullable(requirement);
	}

	private static VisibleLine visibleLine(String line, boolean initiallyInBlockComment) {
		StringBuilder visible= new StringBuilder();
		boolean inBlockComment= initiallyInBlockComment;
		for (int index= 0; index < line.length();) {
			if (inBlockComment) {
				int end= line.indexOf("*/", index); //$NON-NLS-1$
				if (end < 0) {
					return new VisibleLine(visible.toString(), true);
				}
				inBlockComment= false;
				index= end + 2;
				continue;
			}
			if (line.startsWith("//", index)) { //$NON-NLS-1$
				break;
			}
			if (line.startsWith("/*", index)) { //$NON-NLS-1$
				inBlockComment= true;
				index+= 2;
				continue;
			}
			visible.append(line.charAt(index++));
		}
		return new VisibleLine(visible.toString(), inBlockComment);
	}

	private record VisibleLine(String text, boolean inBlockComment) {
	}
}