HintBindingPolicy.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 {@code <!binding-policy: optional|required>} safety contract from
* an ordinary hint program without otherwise interpreting the DSL.
*
* <p>{@link #OPTIONAL} preserves the historical behaviour of ordinary hints:
* a binding-aware guard may degrade when semantic bindings are unavailable.
* {@link #REQUIRED} is the fail-closed mode for ordinary migration programs
* whose correctness depends on overload, owner, type or hierarchy information.
* Plan-aware programs do not need this second declaration because
* {@code <!requires-plan: ...>} already implies required semantic bindings.</p>
*
* <p>Duplicate identical declarations are tolerated. Blank, malformed or
* conflicting declarations are rejected. Commented declarations are ignored.</p>
*
* @since 1.3.3
*/
public enum HintBindingPolicy {
OPTIONAL,
REQUIRED;
private static final String PREFIX= "<!binding-policy"; //$NON-NLS-1$
private static final Pattern DIRECTIVE= Pattern.compile(
"^\\s*<!binding-policy\\s*:\\s*([^>\\r\\n]*)>\\s*$"); //$NON-NLS-1$
/**
* Reads an explicitly declared binding policy.
*
* @param content complete hint program text
* @return the declared policy, or empty when the program uses the compatibility default
* @throws IllegalArgumentException for blank, malformed, unknown or conflicting declarations
*/
public static Optional<HintBindingPolicy> fromContent(String content) {
if (content == null || content.isBlank()) {
return Optional.empty();
}
HintBindingPolicy policy= 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 binding-policy declaration: " + visible); //$NON-NLS-1$
}
String candidateText= matcher.group(1).trim();
if (candidateText.isEmpty()) {
throw new IllegalArgumentException("binding-policy must be optional or required"); //$NON-NLS-1$
}
HintBindingPolicy candidate;
try {
candidate= HintBindingPolicy.valueOf(candidateText.toUpperCase(java.util.Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Unknown binding-policy " + candidateText + "; expected optional or required", e); //$NON-NLS-1$ //$NON-NLS-2$
}
if (policy != null && policy != candidate) {
throw new IllegalArgumentException(
"Conflicting binding-policy declarations: " + policy.name().toLowerCase(java.util.Locale.ROOT) //$NON-NLS-1$
+ " and " + candidate.name().toLowerCase(java.util.Locale.ROOT)); //$NON-NLS-1$
}
policy= candidate;
}
return Optional.ofNullable(policy);
}
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) {
}
}