BuiltInGuardRegistration.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
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.internal;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.sandbox.jdt.triggerpattern.api.GuardContext;
import org.sandbox.jdt.triggerpattern.api.GuardFunction;
import org.sandbox.jdt.triggerpattern.api.SemanticPlanValue;
import org.sandbox.jdt.triggerpattern.api.SemanticPlanValue.ListValue;
import org.sandbox.jdt.triggerpattern.api.SemanticRewritePlan.NodeKey;
/** Canonical registration entry point for built-in guard functions. */
public final class BuiltInGuardRegistration {
enum HierarchyMatch {
MATCH,
NO_MATCH,
UNKNOWN
}
private BuiltInGuardRegistration() {
}
/** Registers built-in guards with strict plan-aware migration guards. */
public static void registerAll(Map<String, GuardFunction> guards) {
BuiltInGuards.registerAll(guards);
guards.put("instanceof", BuiltInGuardRegistration::evaluateInstanceOf); //$NON-NLS-1$
guards.put("subtypeOf", BuiltInGuardRegistration::evaluateSubtypeOf); //$NON-NLS-1$
guards.put("genericTypeIs", BuiltInGuardRegistration::evaluateGenericTypeIs); //$NON-NLS-1$
guards.put("plannedRole", BuiltInGuardRegistration::evaluatePlannedRole); //$NON-NLS-1$
guards.put("enclosingPlannedRole", BuiltInGuardRegistration::evaluateEnclosingPlannedRole); //$NON-NLS-1$
guards.put("plannedValue", BuiltInGuardRegistration::evaluatePlannedValue); //$NON-NLS-1$
guards.put("enclosingPlannedValue", BuiltInGuardRegistration::evaluateEnclosingPlannedValue); //$NON-NLS-1$
guards.put("plannedNodeValue", BuiltInGuardRegistration::evaluatePlannedNodeValue); //$NON-NLS-1$
guards.put("plannedListContains", BuiltInGuardRegistration::evaluatePlannedListContains); //$NON-NLS-1$
guards.put("plannedRelation", BuiltInGuardRegistration::evaluatePlannedRelation); //$NON-NLS-1$
guards.put("plannedRelationValue", BuiltInGuardRegistration::evaluatePlannedRelationValue); //$NON-NLS-1$
guards.put("plannedOutgoingRelation", BuiltInGuardRegistration::evaluatePlannedOutgoingRelation); //$NON-NLS-1$
guards.put("plannedIncomingRelation", BuiltInGuardRegistration::evaluatePlannedIncomingRelation); //$NON-NLS-1$
guards.put("plannedRelationCount", BuiltInGuardRegistration::evaluatePlannedRelationCount); //$NON-NLS-1$
}
private static boolean evaluatePlannedRole(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 1);
return parsed != null && parsed.node() != null && context.getSemanticPlan().hasRole(parsed.node(),
stripQuotes(argument(args, parsed.offset())));
}
private static boolean evaluateEnclosingPlannedRole(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 1);
return parsed != null && parsed.node() != null && context.getSemanticPlan().hasEnclosingRole(parsed.node(),
stripQuotes(argument(args, parsed.offset())));
}
private static boolean evaluatePlannedValue(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 2);
return parsed != null && parsed.node() != null && context.getSemanticPlan().hasValue(parsed.node(),
stripQuotes(argument(args, parsed.offset())), literal(args, parsed.offset() + 1));
}
private static boolean evaluateEnclosingPlannedValue(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 2);
return parsed != null && parsed.node() != null && context.getSemanticPlan().hasEnclosingValue(parsed.node(),
stripQuotes(argument(args, parsed.offset())), literal(args, parsed.offset() + 1));
}
private static boolean evaluatePlannedNodeValue(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 2);
if (parsed == null || parsed.node() == null) {
return false;
}
NodeKey expectedKey= NodeKey.from(context.getBinding(argument(args, parsed.offset() + 1)));
return expectedKey != null && context.getSemanticPlan().hasValue(parsed.node(),
stripQuotes(argument(args, parsed.offset())), SemanticPlanValue.node(expectedKey));
}
private static boolean evaluatePlannedListContains(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 2);
if (parsed == null || parsed.node() == null) {
return false;
}
String name= stripQuotes(argument(args, parsed.offset()));
SemanticPlanValue expected= literal(args, parsed.offset() + 1);
return context.getSemanticPlan().value(parsed.node(), name)
.filter(ListValue.class::isInstance)
.map(ListValue.class::cast)
.map(value -> value.values().contains(expected))
.orElse(false);
}
private static boolean evaluatePlannedRelation(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 2);
if (parsed == null || parsed.node() == null) {
return false;
}
ASTNode target= context.getBinding(argument(args, parsed.offset() + 1));
return target != null && context.getSemanticPlan().hasRelation(parsed.node(),
stripQuotes(argument(args, parsed.offset())), target);
}
private static boolean evaluatePlannedRelationValue(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 4);
if (parsed == null || parsed.node() == null) {
return false;
}
ASTNode target= context.getBinding(argument(args, parsed.offset() + 1));
return target != null && context.getSemanticPlan().hasRelationValue(parsed.node(),
stripQuotes(argument(args, parsed.offset())), target,
stripQuotes(argument(args, parsed.offset() + 2)), literal(args, parsed.offset() + 3));
}
private static boolean evaluatePlannedOutgoingRelation(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 1);
return parsed != null && parsed.node() != null && !context.getSemanticPlan()
.outgoing(parsed.node(), stripQuotes(argument(args, parsed.offset()))).isEmpty();
}
private static boolean evaluatePlannedIncomingRelation(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 1);
return parsed != null && parsed.node() != null && !context.getSemanticPlan()
.incoming(parsed.node(), stripQuotes(argument(args, parsed.offset()))).isEmpty();
}
private static boolean evaluatePlannedRelationCount(GuardContext context, Object... args) {
PlanNodeArguments parsed= planNodeArguments(context, args, 2);
if (parsed == null || parsed.node() == null) {
return false;
}
try {
return context.getSemanticPlan()
.outgoing(parsed.node(), stripQuotes(argument(args, parsed.offset()))).size()
== Integer.parseInt(argument(args, parsed.offset() + 1));
} catch (NumberFormatException e) {
return false;
}
}
/**
* Resolves the optional leading bound-node argument shared by all plan guards.
* Extra or missing arguments are rejected rather than ignored.
*/
private static PlanNodeArguments planNodeArguments(GuardContext context, Object[] args, int implicitArity) {
if (args == null) {
return null;
}
if (args.length == implicitArity) {
return new PlanNodeArguments(context.getMatchedNode(), 0);
}
if (args.length == implicitArity + 1) {
return new PlanNodeArguments(context.getBinding(argument(args, 0)), 1);
}
return null;
}
private static boolean evaluateInstanceOf(GuardContext context, Object... args) {
if (args.length < 2) {
return false;
}
ASTNode node= context.getBinding(argument(args, 0));
if (node == null) {
return false;
}
ITypeBinding binding= resolveTypeBinding(node);
if (binding == null) {
context.markUnknown("instanceof", //$NON-NLS-1$
"Cannot resolve the type of " + argument(args, 0)); //$NON-NLS-1$
return true;
}
if (binding.isRecovered()) {
context.markUnknown("instanceof", //$NON-NLS-1$
"The recovered type of " + argument(args, 0) + " is incomplete"); //$NON-NLS-1$ //$NON-NLS-2$
}
String expectedType= stripQuotes(argument(args, 1));
if (expectedType.endsWith("[]") && expectedType.length() > 2) { //$NON-NLS-1$
return binding.isArray() && matchesTypeName(binding.getElementType(),
expectedType.substring(0, expectedType.length() - 2));
}
return matchesTypeName(binding, expectedType);
}
private static boolean evaluateSubtypeOf(GuardContext context, Object... args) {
if (args.length < 2) {
return false;
}
ASTNode node= context.getBinding(argument(args, 0));
if (node == null) {
return false;
}
ITypeBinding binding= resolveTypeBinding(node);
if (binding == null) {
context.markUnknown("subtypeOf", //$NON-NLS-1$
"Cannot resolve the type hierarchy of " + argument(args, 0)); //$NON-NLS-1$
return true;
}
HierarchyMatch match= subtypeMatch(binding, stripQuotes(argument(args, 1)), new HashSet<>());
if (match == HierarchyMatch.UNKNOWN) {
context.markUnknown("subtypeOf", //$NON-NLS-1$
"Cannot resolve the complete type hierarchy of " + argument(args, 0)); //$NON-NLS-1$
return true;
}
return match == HierarchyMatch.MATCH;
}
static HierarchyMatch subtypeMatch(ITypeBinding binding, String expectedType, Set<String> visited) {
if (binding == null) {
return HierarchyMatch.NO_MATCH;
}
if (binding.isRecovered()) {
return HierarchyMatch.UNKNOWN;
}
ITypeBinding declaration= binding.getTypeDeclaration();
if (declaration == null || declaration.isRecovered()) {
return HierarchyMatch.UNKNOWN;
}
String key= declaration.getKey();
String identity= key == null || key.isBlank() ? declaration.getQualifiedName() : key;
if (identity != null && !identity.isBlank() && !visited.add(identity)) {
return HierarchyMatch.NO_MATCH;
}
if (matchesTypeName(declaration, expectedType)) {
return HierarchyMatch.MATCH;
}
boolean unknown= false;
HierarchyMatch superclassMatch= subtypeMatch(declaration.getSuperclass(), expectedType, visited);
if (superclassMatch == HierarchyMatch.MATCH) {
return HierarchyMatch.MATCH;
}
unknown= superclassMatch == HierarchyMatch.UNKNOWN;
for (ITypeBinding iface : declaration.getInterfaces()) {
HierarchyMatch interfaceMatch= subtypeMatch(iface, expectedType, visited);
if (interfaceMatch == HierarchyMatch.MATCH) {
return HierarchyMatch.MATCH;
}
unknown|= interfaceMatch == HierarchyMatch.UNKNOWN;
}
return unknown ? HierarchyMatch.UNKNOWN : HierarchyMatch.NO_MATCH;
}
private static boolean evaluateGenericTypeIs(GuardContext context, Object... args) {
if (args.length < 3) {
return false;
}
ASTNode node= context.getBinding(argument(args, 0));
if (node == null) {
return false;
}
int index;
try {
index= Integer.parseInt(argument(args, 1));
} catch (NumberFormatException e) {
return false;
}
ITypeBinding binding= resolveTypeBinding(node);
if (binding == null) {
context.markUnknown("genericTypeIs", //$NON-NLS-1$
"Cannot resolve generic type arguments for " + argument(args, 0)); //$NON-NLS-1$
return true;
}
if (binding.isRecovered()) {
context.markUnknown("genericTypeIs", //$NON-NLS-1$
"The recovered generic type of " + argument(args, 0) + " is incomplete"); //$NON-NLS-1$ //$NON-NLS-2$
}
if (index < 0) {
return false;
}
if (binding.isRawType()) {
context.markUnknown("genericTypeIs", //$NON-NLS-1$
"Generic type arguments are erased for " + argument(args, 0)); //$NON-NLS-1$
return false;
}
ITypeBinding[] arguments= binding.getTypeArguments();
if (index >= arguments.length) {
return false;
}
if (arguments[index] == null) {
context.markUnknown("genericTypeIs", //$NON-NLS-1$
"Cannot resolve generic type argument " + index + " for " + argument(args, 0)); //$NON-NLS-1$ //$NON-NLS-2$
return false;
}
if (arguments[index].isRecovered()) {
context.markUnknown("genericTypeIs", //$NON-NLS-1$
"Generic type argument " + index + " for " + argument(args, 0) + " is recovered"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
return matchesTypeName(arguments[index], stripQuotes(argument(args, 2)));
}
private static SemanticPlanValue literal(Object[] args, int index) {
return SemanticPlanValue.fromGuardLiteral(argument(args, index));
}
private static String argument(Object[] args, int index) {
if (args == null || index < 0 || index >= args.length || args[index] == null) {
return ""; //$NON-NLS-1$
}
return args[index].toString();
}
private static ITypeBinding resolveTypeBinding(ASTNode node) {
if (node instanceof Expression expression) {
return expression.resolveTypeBinding();
}
if (node instanceof Type type) {
return type.resolveBinding();
}
if (node instanceof SingleVariableDeclaration declaration) {
IVariableBinding binding= declaration.resolveBinding();
return binding == null ? declaration.getType().resolveBinding() : binding.getType();
}
if (node instanceof VariableDeclarationFragment fragment) {
IVariableBinding binding= fragment.resolveBinding();
return binding == null ? null : binding.getType();
}
return null;
}
private static boolean matchesTypeName(ITypeBinding binding, String expectedType) {
if (binding == null || expectedType == null || expectedType.isBlank()) {
return false;
}
ITypeBinding declaration= binding.getTypeDeclaration();
if (declaration == null) {
return false;
}
String qualifiedName= declaration.getQualifiedName();
return expectedType.equals(qualifiedName) || expectedType.equals(declaration.getName());
}
private static String stripQuotes(String value) {
String stripped= value == null ? "" : value.trim(); //$NON-NLS-1$
if (stripped.length() >= 2 && stripped.startsWith("\"") && stripped.endsWith("\"")) { //$NON-NLS-1$ //$NON-NLS-2$
return stripped.substring(1, stripped.length() - 1);
}
return stripped;
}
private record PlanNodeArguments(ASTNode node, int offset) {
}
}