SemanticRewritePlan.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.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.ConstructorInvocation;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
/**
* Immutable binding-stable authorization and fact plan for plan-aware hints.
*
* <p>A named contract identifies the semantic planner that produced the plan.
* Roles authorize broad rewrite categories. Typed values describe properties
* such as runner kind, lifecycle scope, matrix coordinates or ordering policy.
* Ordered directed relations describe graph structure such as suite membership,
* providers, wrappers and inheritance without introducing project-specific
* concepts into the shared engine.</p>
*
* <p>Type, method and field targets are identified by declaration binding keys.
* Call sites additionally retain their source range so multiple calls to the
* same API remain distinct. Missing contracts or bindings never authorize a
* plan-aware rewrite.</p>
*/
public final class SemanticRewritePlan {
private final String contractId;
private final Map<NodeKey, Set<String>> rolesByNode;
private final Map<NodeKey, Map<String, SemanticPlanValue>> valuesByNode;
private final List<SemanticPlanRelation> relations;
private final Map<NodeKey, Map<String, List<SemanticPlanRelation>>> outgoingRelations;
private final Map<NodeKey, Map<String, List<SemanticPlanRelation>>> incomingRelations;
/** Creates a defensive immutable plan. */
public SemanticRewritePlan(String contractId, Map<NodeKey, Set<String>> rolesByNode,
Map<NodeKey, Map<String, SemanticPlanValue>> valuesByNode,
List<SemanticPlanRelation> relations) {
this.contractId= contractId == null || contractId.isBlank() ? null : contractId.trim();
this.rolesByNode= immutableRoles(rolesByNode);
this.valuesByNode= immutableValues(valuesByNode);
this.relations= List.copyOf(relations == null ? List.of() : relations);
validateValueKinds(this.valuesByNode, this.relations);
this.outgoingRelations= indexRelations(this.relations, true);
this.incomingRelations= indexRelations(this.relations, false);
}
/** Backward-compatible constructor for role-only contracted plans. */
public SemanticRewritePlan(String contractId, Map<NodeKey, Set<String>> rolesByNode) {
this(contractId, rolesByNode, Map.of(), List.of());
}
/** Creates a contracted plan with roles and typed values but no relations. */
public SemanticRewritePlan(String contractId, Map<NodeKey, Set<String>> rolesByNode,
Map<NodeKey, Map<String, SemanticPlanValue>> valuesByNode) {
this(contractId, rolesByNode, valuesByNode, List.of());
}
/** Backward-compatible constructor for uncontracted guard-only plans. */
public SemanticRewritePlan(Map<NodeKey, Set<String>> rolesByNode) {
this(null, rolesByNode, Map.of(), List.of());
}
/** Returns an empty, uncontracted plan. */
public static SemanticRewritePlan empty() {
return new SemanticRewritePlan(null, Map.of(), Map.of(), List.of());
}
/** Creates a mutable builder without an execution contract. */
public static Builder builder() {
return new Builder(null);
}
/** Creates a mutable builder bound to a plan-aware hint contract. */
public static Builder builder(String contractId) {
return new Builder(contractId);
}
public String contractId() {
return contractId;
}
public Map<NodeKey, Set<String>> rolesByNode() {
return rolesByNode;
}
public Map<NodeKey, Map<String, SemanticPlanValue>> valuesByNode() {
return valuesByNode;
}
/** Returns all relation occurrences in planner order, including duplicates. */
public List<SemanticPlanRelation> relations() {
return relations;
}
/** Returns whether this plan contains no roles, values or relations. */
public boolean isEmpty() {
return rolesByNode.isEmpty() && valuesByNode.isEmpty() && relations.isEmpty();
}
/** Returns whether this plan was produced for the exact named contract. */
public boolean satisfiesContract(String requiredContractId) {
return contractId != null && requiredContractId != null
&& contractId.equals(requiredContractId.trim());
}
/** Returns whether the exact AST target has the requested role. */
public boolean hasRole(ASTNode node, String role) {
NodeKey key= NodeKey.from(node);
return key != null && role != null && rolesByNode.getOrDefault(key, Set.of()).contains(role);
}
/** Returns whether an enclosing semantic declaration has the requested role. */
public boolean hasEnclosingRole(ASTNode node, String role) {
ASTNode current= node;
while (current != null) {
if (hasRole(current, role)) {
return true;
}
current= current.getParent();
}
return false;
}
/** Returns one typed value for the exact stable node key. */
public Optional<SemanticPlanValue> value(NodeKey key, String name) {
if (key == null || name == null) {
return Optional.empty();
}
return Optional.ofNullable(valuesByNode.getOrDefault(key, Map.of()).get(name));
}
/** Returns one typed value for the exact AST target. */
public Optional<SemanticPlanValue> value(ASTNode node, String name) {
return value(NodeKey.from(node), name);
}
/** Returns the first typed value found on the node or an enclosing declaration. */
public Optional<SemanticPlanValue> enclosingValue(ASTNode node, String name) {
ASTNode current= node;
while (current != null) {
Optional<SemanticPlanValue> value= value(current, name);
if (value.isPresent()) {
return value;
}
current= current.getParent();
}
return Optional.empty();
}
/** Returns whether the exact AST target has the supplied typed value. */
public boolean hasValue(ASTNode node, String name, SemanticPlanValue expected) {
return expected != null && value(node, name).filter(expected::equals).isPresent();
}
/** Returns whether the node or an enclosing declaration has the supplied value. */
public boolean hasEnclosingValue(ASTNode node, String name, SemanticPlanValue expected) {
return expected != null && enclosingValue(node, name).filter(expected::equals).isPresent();
}
/** Returns all outgoing relations of the requested kind in declared order. */
public List<SemanticPlanRelation> outgoing(NodeKey source, String kind) {
if (source == null || kind == null) {
return List.of();
}
return outgoingRelations.getOrDefault(source, Map.of()).getOrDefault(kind, List.of());
}
/** Returns all outgoing relations for an AST target in declared order. */
public List<SemanticPlanRelation> outgoing(ASTNode source, String kind) {
return outgoing(NodeKey.from(source), kind);
}
/** Returns all incoming relations of the requested kind in declared order. */
public List<SemanticPlanRelation> incoming(NodeKey target, String kind) {
if (target == null || kind == null) {
return List.of();
}
return incomingRelations.getOrDefault(target, Map.of()).getOrDefault(kind, List.of());
}
/** Returns all incoming relations for an AST target in declared order. */
public List<SemanticPlanRelation> incoming(ASTNode target, String kind) {
return incoming(NodeKey.from(target), kind);
}
/** Returns whether an exact directed relation exists. */
public boolean hasRelation(ASTNode source, String kind, ASTNode target) {
NodeKey sourceKey= NodeKey.from(source);
NodeKey targetKey= NodeKey.from(target);
return sourceKey != null && targetKey != null && outgoing(sourceKey, kind).stream()
.anyMatch(relation -> targetKey.equals(relation.target()));
}
/** Returns whether an exact relation has the supplied typed attribute. */
public boolean hasRelationValue(ASTNode source, String kind, ASTNode target, String name,
SemanticPlanValue expected) {
NodeKey sourceKey= NodeKey.from(source);
NodeKey targetKey= NodeKey.from(target);
return sourceKey != null && targetKey != null && expected != null
&& outgoing(sourceKey, kind).stream()
.filter(relation -> targetKey.equals(relation.target()))
.anyMatch(relation -> relation.hasAttribute(name, expected));
}
private static Map<NodeKey, Set<String>> immutableRoles(Map<NodeKey, Set<String>> source) {
Map<NodeKey, Set<String>> copy= new LinkedHashMap<>();
if (source != null) {
source.forEach((key, roles) -> copy.put(Objects.requireNonNull(key),
Set.copyOf(roles == null ? Set.of() : roles)));
}
return Map.copyOf(copy);
}
private static Map<NodeKey, Map<String, SemanticPlanValue>> immutableValues(
Map<NodeKey, Map<String, SemanticPlanValue>> source) {
Map<NodeKey, Map<String, SemanticPlanValue>> copy= new LinkedHashMap<>();
if (source != null) {
source.forEach((key, values) -> {
Map<String, SemanticPlanValue> nodeValues= new LinkedHashMap<>();
if (values != null) {
values.forEach((name, value) -> nodeValues.put(requireName(name, "Plan value"), //$NON-NLS-1$
Objects.requireNonNull(value)));
}
copy.put(Objects.requireNonNull(key), Map.copyOf(nodeValues));
});
}
return Map.copyOf(copy);
}
private static Map<NodeKey, Map<String, List<SemanticPlanRelation>>> indexRelations(
List<SemanticPlanRelation> relations, boolean outgoing) {
Map<NodeKey, Map<String, List<SemanticPlanRelation>>> mutable= new LinkedHashMap<>();
for (SemanticPlanRelation relation : relations) {
NodeKey node= outgoing ? relation.source() : relation.target();
mutable.computeIfAbsent(node, ignored -> new LinkedHashMap<>())
.computeIfAbsent(relation.kind(), ignored -> new ArrayList<>()).add(relation);
}
Map<NodeKey, Map<String, List<SemanticPlanRelation>>> result= new LinkedHashMap<>();
mutable.forEach((node, byKind) -> {
Map<String, List<SemanticPlanRelation>> immutableByKind= new LinkedHashMap<>();
byKind.forEach((kind, indexedRelations) ->
immutableByKind.put(kind, List.copyOf(indexedRelations)));
result.put(node, Map.copyOf(immutableByKind));
});
return Map.copyOf(result);
}
private static void validateValueKinds(Map<NodeKey, Map<String, SemanticPlanValue>> values,
List<SemanticPlanRelation> relations) {
Map<String, String> factKinds= new LinkedHashMap<>();
values.values().forEach(nodeValues -> nodeValues.forEach((name, value) ->
validateKind(factKinds, name, typeSignature(value), "plan value"))); //$NON-NLS-1$
Map<String, String> relationKinds= new LinkedHashMap<>();
for (SemanticPlanRelation relation : relations) {
relation.attributes().forEach((name, value) -> validateKind(relationKinds,
relation.kind() + "." + name, typeSignature(value), "relation attribute")); //$NON-NLS-1$ //$NON-NLS-2$
}
}
private static void validateKind(Map<String, String> kinds, String name, String kind, String label) {
String previous= kinds.putIfAbsent(name, kind);
if (previous != null && !previous.equals(kind)) {
throw new IllegalArgumentException("Conflicting types for " + label + " " + name //$NON-NLS-1$ //$NON-NLS-2$
+ ": " + previous + " and " + kind); //$NON-NLS-1$ //$NON-NLS-2$
}
}
private static String typeSignature(SemanticPlanValue value) {
return value.kind().name();
}
private static String requireName(String value, String label) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(label + " name must not be blank"); //$NON-NLS-1$
}
return value.trim();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof SemanticRewritePlan plan)) {
return false;
}
return Objects.equals(contractId, plan.contractId)
&& rolesByNode.equals(plan.rolesByNode)
&& valuesByNode.equals(plan.valuesByNode)
&& relations.equals(plan.relations);
}
@Override
public int hashCode() {
return Objects.hash(contractId, rolesByNode, valuesByNode, relations);
}
@Override
public String toString() {
return "SemanticRewritePlan[contractId=" + contractId + ", rolesByNode=" + rolesByNode //$NON-NLS-1$ //$NON-NLS-2$
+ ", valuesByNode=" + valuesByNode + ", relations=" + relations + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
/** Stable supported semantic node kinds. */
public enum NodeKind {
TYPE,
METHOD,
FIELD,
INVOCATION
}
/** Stable key used to re-identify an authorized AST target. */
public record NodeKey(NodeKind kind, String bindingKey, int sourceStart, int sourceLength) {
public NodeKey {
Objects.requireNonNull(kind);
Objects.requireNonNull(bindingKey);
}
public static NodeKey type(String bindingKey) {
return bindingKey == null ? null : new NodeKey(NodeKind.TYPE, bindingKey, -1, -1);
}
public static NodeKey method(String bindingKey) {
return bindingKey == null ? null : new NodeKey(NodeKind.METHOD, bindingKey, -1, -1);
}
public static NodeKey field(String bindingKey) {
return bindingKey == null ? null : new NodeKey(NodeKind.FIELD, bindingKey, -1, -1);
}
public static NodeKey invocation(String bindingKey, int sourceStart, int sourceLength) {
return bindingKey == null ? null
: new NodeKey(NodeKind.INVOCATION, bindingKey, sourceStart, sourceLength);
}
/** Resolves the canonical plan key for a supported AST node. */
public static NodeKey from(ASTNode node) {
if (node instanceof TypeDeclaration type) {
return type(typeKey(type.resolveBinding()));
}
if (node instanceof EnumDeclaration type) {
return type(typeKey(type.resolveBinding()));
}
if (node instanceof AnnotationTypeDeclaration type) {
return type(typeKey(type.resolveBinding()));
}
if (node instanceof RecordDeclaration type) {
return type(typeKey(type.resolveBinding()));
}
if (node instanceof MethodDeclaration method) {
return method(methodKey(method.resolveBinding()));
}
if (node instanceof FieldDeclaration field) {
return field.fragments().size() == 1 ? from((ASTNode) field.fragments().get(0)) : null;
}
if (node instanceof VariableDeclarationFragment field) {
return field(fieldKey(field.resolveBinding()));
}
if (node instanceof MethodInvocation invocation) {
return invocation(methodKey(invocation.resolveMethodBinding()), invocation.getStartPosition(),
invocation.getLength());
}
if (node instanceof SuperMethodInvocation invocation) {
return invocation(methodKey(invocation.resolveMethodBinding()), invocation.getStartPosition(),
invocation.getLength());
}
if (node instanceof ClassInstanceCreation invocation) {
return invocation(methodKey(invocation.resolveConstructorBinding()), invocation.getStartPosition(),
invocation.getLength());
}
if (node instanceof ConstructorInvocation invocation) {
return invocation(methodKey(invocation.resolveConstructorBinding()), invocation.getStartPosition(),
invocation.getLength());
}
if (node instanceof SuperConstructorInvocation invocation) {
return invocation(methodKey(invocation.resolveConstructorBinding()), invocation.getStartPosition(),
invocation.getLength());
}
if (node instanceof SimpleName name) {
IBinding binding= name.resolveBinding();
if (binding instanceof IMethodBinding methodBinding) {
return method(methodKey(methodBinding));
}
if (binding instanceof ITypeBinding typeBinding) {
return type(typeKey(typeBinding));
}
if (binding instanceof IVariableBinding variableBinding) {
return field(fieldKey(variableBinding));
}
}
return null;
}
private static String typeKey(ITypeBinding binding) {
if (binding == null) {
return null;
}
ITypeBinding erasure= binding.getErasure();
ITypeBinding declaration= (erasure == null ? binding : erasure).getTypeDeclaration();
return declaration == null ? null : declaration.getKey();
}
private static String methodKey(IMethodBinding binding) {
return binding == null ? null : binding.getMethodDeclaration().getKey();
}
private static String fieldKey(IVariableBinding binding) {
if (binding == null || !binding.isField()) {
return null;
}
return binding.getVariableDeclaration().getKey();
}
}
/** Builder used by semantic planners. */
public static final class Builder {
private final String contractId;
private final Map<NodeKey, Set<String>> roles= new LinkedHashMap<>();
private final Map<NodeKey, Map<String, SemanticPlanValue>> values= new LinkedHashMap<>();
private final List<SemanticPlanRelation> relations= new ArrayList<>();
private Builder(String contractId) {
this.contractId= contractId;
}
/** Adds one authorization role to a stable node. */
public Builder add(NodeKey key, String role) {
if (key != null && role != null && !role.isBlank()) {
roles.computeIfAbsent(key, ignored -> new LinkedHashSet<>()).add(role);
}
return this;
}
/** Adds one typed fact and rejects conflicting duplicate definitions. */
public Builder put(NodeKey key, String name, SemanticPlanValue value) {
if (key == null) {
return this;
}
String factName= requireName(name, "Plan value"); //$NON-NLS-1$
SemanticPlanValue factValue= Objects.requireNonNull(value);
Map<String, SemanticPlanValue> nodeValues=
values.computeIfAbsent(key, ignored -> new LinkedHashMap<>());
SemanticPlanValue previous= nodeValues.putIfAbsent(factName, factValue);
if (previous != null && !previous.equals(factValue)) {
throw new IllegalArgumentException("Conflicting values for " + factName + " on " + key); //$NON-NLS-1$ //$NON-NLS-2$
}
return this;
}
public Builder putString(NodeKey key, String name, String value) {
return put(key, name, SemanticPlanValue.string(value));
}
public Builder putBoolean(NodeKey key, String name, boolean value) {
return put(key, name, SemanticPlanValue.bool(value));
}
public Builder putInteger(NodeKey key, String name, long value) {
return put(key, name, SemanticPlanValue.integer(value));
}
public Builder putNode(NodeKey key, String name, NodeKey value) {
return put(key, name, SemanticPlanValue.node(value));
}
public Builder putList(NodeKey key, String name, SemanticPlanValue... value) {
return put(key, name, SemanticPlanValue.list(value));
}
/** Adds one ordered relation occurrence. Duplicate occurrences are preserved. */
public Builder relate(NodeKey source, String kind, NodeKey target) {
return relate(source, kind, target, Map.of());
}
/** Adds one ordered relation occurrence with typed attributes. */
public Builder relate(NodeKey source, String kind, NodeKey target,
Map<String, SemanticPlanValue> attributes) {
relations.add(new SemanticPlanRelation(kind, Objects.requireNonNull(source),
Objects.requireNonNull(target), attributes));
return this;
}
public SemanticRewritePlan build() {
return new SemanticRewritePlan(contractId, roles, values, relations);
}
}
}