BuiltInStructuredRewriteActions.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.cleanup.actions;
import java.util.List;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.sandbox.jdt.triggerpattern.api.RewriteActionValue;
import org.sandbox.jdt.triggerpattern.api.StructuredRewriteAction;
/** Canonical built-in runtime handlers for the standard action catalog. */
final class BuiltInStructuredRewriteActions {
private BuiltInStructuredRewriteActions() {
}
static void registerAll(StructuredRewriteActionRegistry registry) {
registry.register("addAnnotation", BuiltInStructuredRewriteActions::addAnnotation); //$NON-NLS-1$
registry.register("removeAnnotation", BuiltInStructuredRewriteActions::removeAnnotation); //$NON-NLS-1$
registry.register("addModifier", BuiltInStructuredRewriteActions::addModifier); //$NON-NLS-1$
registry.register("removeModifier", BuiltInStructuredRewriteActions::removeModifier); //$NON-NLS-1$
registry.register("removeSupertype", BuiltInStructuredRewriteActions::removeSupertype); //$NON-NLS-1$
registry.register("replaceSupertype", BuiltInStructuredRewriteActions::replaceSupertype); //$NON-NLS-1$
registry.register("removeDeclaration", BuiltInStructuredRewriteActions::removeDeclaration); //$NON-NLS-1$
registry.register("qualifyInvocation", BuiltInStructuredRewriteActions::qualifyInvocation); //$NON-NLS-1$
}
private static void addAnnotation(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
BodyDeclaration declaration= declarationTarget(context,
action.requiredArgument("target")); //$NON-NLS-1$
String annotationName= context.resolveString(action.requiredArgument("annotation")); //$NON-NLS-1$
ensureAnnotationMissing(declaration, annotationName, context);
AST ast= context.ast();
String importedName= context.cuRewrite().getImportRewrite().addImport(annotationName);
Annotation annotation;
RewriteActionValue value= action.arguments().get("value"); //$NON-NLS-1$
if (value == null) {
MarkerAnnotation marker= ast.newMarkerAnnotation();
marker.setTypeName(ast.newName(importedName));
annotation= marker;
} else {
SingleMemberAnnotation single= ast.newSingleMemberAnnotation();
single.setTypeName(ast.newName(importedName));
single.setValue(context.createExpression(value));
annotation= single;
}
modifiers(context.cuRewrite().getASTRewrite(), declaration, context)
.insertFirst(annotation, context.editGroup());
}
private static void removeAnnotation(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
BodyDeclaration declaration= declarationTarget(context,
action.requiredArgument("target")); //$NON-NLS-1$
String expected= context.resolveString(action.requiredArgument("annotation")); //$NON-NLS-1$
String expectedSimple= simpleName(expected);
for (Object modifier : declaration.modifiers()) {
if (modifier instanceof Annotation annotation) {
String actual= annotationName(annotation);
if (expected.equals(actual) || expectedSimple.equals(simpleName(actual))) {
context.cuRewrite().getASTRewrite().remove(annotation, context.editGroup());
context.cuRewrite().getImportRemover().registerRemovedNode(annotation);
context.cuRewrite().getImportRemover()
.applyRemoves(context.cuRewrite().getImportRewrite());
return;
}
}
}
throw context.failure("Structured action cannot find annotation " + expected); //$NON-NLS-1$
}
private static void addModifier(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
BodyDeclaration declaration= declarationTarget(context,
action.requiredArgument("target")); //$NON-NLS-1$
Modifier.ModifierKeyword keyword= modifierKeyword(
context.resolveString(action.requiredArgument("modifier")), context); //$NON-NLS-1$
for (Object modifier : declaration.modifiers()) {
if (modifier instanceof Modifier existing && existing.getKeyword() == keyword) {
throw context.failure("Structured action modifier already exists: " + keyword); //$NON-NLS-1$
}
}
modifiers(context.cuRewrite().getASTRewrite(), declaration, context)
.insertLast(context.ast().newModifier(keyword), context.editGroup());
}
private static void removeModifier(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
BodyDeclaration declaration= declarationTarget(context,
action.requiredArgument("target")); //$NON-NLS-1$
Modifier.ModifierKeyword keyword= modifierKeyword(
context.resolveString(action.requiredArgument("modifier")), context); //$NON-NLS-1$
for (Object modifier : declaration.modifiers()) {
if (modifier instanceof Modifier existing && existing.getKeyword() == keyword) {
context.cuRewrite().getASTRewrite().remove(existing, context.editGroup());
return;
}
}
throw context.failure("Structured action cannot find modifier " + keyword); //$NON-NLS-1$
}
private static void removeSupertype(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
ASTNode target= context.resolveAuthorizedNode(action.requiredArgument("target")); //$NON-NLS-1$
String expected= context.resolveString(action.requiredArgument("type")); //$NON-NLS-1$
rewriteSupertype(target, expected, null, context);
}
private static void replaceSupertype(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
ASTNode target= context.resolveAuthorizedNode(action.requiredArgument("target")); //$NON-NLS-1$
String expected= context.resolveString(action.requiredArgument("type")); //$NON-NLS-1$
String replacement= context.resolveString(action.requiredArgument("replacement")); //$NON-NLS-1$
rewriteSupertype(target, expected, replacement, context);
}
private static void removeDeclaration(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
ASTNode target= context.resolveAuthorizedNode(action.requiredArgument("target")); //$NON-NLS-1$
ASTNode removable= exactDeclarationTarget(target, context);
ASTRewrite rewrite= context.cuRewrite().getASTRewrite();
if (removable instanceof VariableDeclarationFragment fragment
&& fragment.getParent() instanceof FieldDeclaration field
&& field.fragments().size() > 1) {
rewrite.getListRewrite(field, FieldDeclaration.FRAGMENTS_PROPERTY)
.remove(fragment, context.editGroup());
} else {
rewrite.remove(removable, context.editGroup());
context.cuRewrite().getImportRemover().registerRemovedNode(removable);
context.cuRewrite().getImportRemover()
.applyRemoves(context.cuRewrite().getImportRewrite());
}
}
private static void qualifyInvocation(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
ASTNode target= context.resolveAuthorizedNode(action.requiredArgument("target")); //$NON-NLS-1$
MethodInvocation invocation= invocationTarget(target, context);
IMethodBinding binding= invocation.resolveMethodBinding();
if (binding == null || !Modifier.isStatic(binding.getModifiers())) {
throw context.failure("qualifyInvocation requires one resolved static method"); //$NON-NLS-1$
}
if (invocation.getExpression() != null) {
throw context.failure("qualifyInvocation target is already qualified"); //$NON-NLS-1$
}
String owner= context.resolveString(action.requiredArgument("owner")); //$NON-NLS-1$
String imported= context.cuRewrite().getImportRewrite().addImport(owner);
context.cuRewrite().getASTRewrite().set(invocation, MethodInvocation.EXPRESSION_PROPERTY,
context.ast().newName(imported), context.editGroup());
}
private static BodyDeclaration declarationTarget(StructuredRewriteActionContext context,
RewriteActionValue value) throws CoreException {
ASTNode current= context.resolveAuthorizedNode(value);
while (current != null) {
if (current instanceof BodyDeclaration declaration) {
if (declaration instanceof FieldDeclaration field && field.fragments().size() != 1) {
throw context.failure(
"Declaration-wide action is ambiguous for a multi-fragment field"); //$NON-NLS-1$
}
return declaration;
}
current= current.getParent();
}
throw context.failure("Structured action target is not a Java declaration"); //$NON-NLS-1$
}
private static ASTNode exactDeclarationTarget(ASTNode target,
StructuredRewriteActionContext context) throws CoreException {
ASTNode current= target;
while (current instanceof SimpleName) {
current= current.getParent();
}
if (current instanceof VariableDeclarationFragment || current instanceof BodyDeclaration) {
return current;
}
while (current != null && !(current instanceof BodyDeclaration)) {
current= current.getParent();
}
if (current == null) {
throw context.failure("removeDeclaration target is not a removable declaration"); //$NON-NLS-1$
}
return current;
}
private static MethodInvocation invocationTarget(ASTNode target,
StructuredRewriteActionContext context) throws CoreException {
if (target instanceof MethodInvocation invocation) {
return invocation;
}
if (target instanceof SimpleName name && name.getParent() instanceof MethodInvocation invocation
&& invocation.getName() == name) {
return invocation;
}
throw context.failure("Structured action target is not an exact method invocation"); //$NON-NLS-1$
}
private static void rewriteSupertype(ASTNode target, String expected, String replacement,
StructuredRewriteActionContext context) throws CoreException {
ASTNode current= target;
while (current != null && !(current instanceof TypeDeclaration)
&& !(current instanceof EnumDeclaration) && !(current instanceof RecordDeclaration)) {
current= current.getParent();
}
if (current instanceof TypeDeclaration type) {
Type superclass= type.getSuperclassType();
if (superclass != null && matchesType(superclass, expected)) {
rewriteType(superclass, replacement, context);
return;
}
if (rewriteInterface(type.superInterfaceTypes(), expected, replacement, context)) {
return;
}
} else if (current instanceof EnumDeclaration type) {
if (rewriteInterface(type.superInterfaceTypes(), expected, replacement, context)) {
return;
}
} else if (current instanceof RecordDeclaration type) {
if (rewriteInterface(type.superInterfaceTypes(), expected, replacement, context)) {
return;
}
}
throw context.failure("Structured action cannot find supertype " + expected); //$NON-NLS-1$
}
private static boolean rewriteInterface(List<?> interfaces, String expected, String replacement,
StructuredRewriteActionContext context) throws CoreException {
for (Object candidate : interfaces) {
if (candidate instanceof Type type && matchesType(type, expected)) {
rewriteType(type, replacement, context);
return true;
}
}
return false;
}
private static void rewriteType(Type existing, String replacement,
StructuredRewriteActionContext context) {
if (replacement == null) {
context.cuRewrite().getASTRewrite().remove(existing, context.editGroup());
} else {
String imported= context.cuRewrite().getImportRewrite().addImport(replacement);
Type newType= context.ast().newSimpleType(context.ast().newName(imported));
context.cuRewrite().getASTRewrite().replace(existing, newType, context.editGroup());
}
context.cuRewrite().getImportRemover().registerRemovedNode(existing);
context.cuRewrite().getImportRemover().applyRemoves(context.cuRewrite().getImportRewrite());
}
private static boolean matchesType(Type type, String expected) {
ITypeBinding binding= type.resolveBinding();
if (binding == null) {
return false;
}
ITypeBinding erasure= binding.getErasure();
ITypeBinding declaration= (erasure == null ? binding : erasure).getTypeDeclaration();
return declaration != null && (expected.equals(declaration.getQualifiedName())
|| expected.equals(declaration.getName()));
}
private static ListRewrite modifiers(ASTRewrite rewrite, BodyDeclaration declaration,
StructuredRewriteActionContext context) throws CoreException {
ChildListPropertyDescriptor property;
if (declaration instanceof TypeDeclaration) {
property= TypeDeclaration.MODIFIERS2_PROPERTY;
} else if (declaration instanceof EnumDeclaration) {
property= EnumDeclaration.MODIFIERS2_PROPERTY;
} else if (declaration instanceof AnnotationTypeDeclaration) {
property= AnnotationTypeDeclaration.MODIFIERS2_PROPERTY;
} else if (declaration instanceof RecordDeclaration) {
property= RecordDeclaration.MODIFIERS2_PROPERTY;
} else if (declaration instanceof MethodDeclaration) {
property= MethodDeclaration.MODIFIERS2_PROPERTY;
} else if (declaration instanceof FieldDeclaration) {
property= FieldDeclaration.MODIFIERS2_PROPERTY;
} else {
throw context.failure("Unsupported declaration modifiers: " + declaration.getClass().getName()); //$NON-NLS-1$
}
return rewrite.getListRewrite(declaration, property);
}
private static Modifier.ModifierKeyword modifierKeyword(String value,
StructuredRewriteActionContext context) throws CoreException {
Modifier.ModifierKeyword keyword= Modifier.ModifierKeyword.toKeyword(value);
if (keyword == null) {
throw context.failure("Unknown Java modifier " + value); //$NON-NLS-1$
}
return keyword;
}
private static void ensureAnnotationMissing(BodyDeclaration declaration, String expected,
StructuredRewriteActionContext context) throws CoreException {
String expectedSimple= simpleName(expected);
for (Object modifier : declaration.modifiers()) {
if (modifier instanceof Annotation annotation) {
String actual= annotationName(annotation);
if (expected.equals(actual) || expectedSimple.equals(simpleName(actual))) {
throw context.failure("Structured action annotation already exists: " + expected); //$NON-NLS-1$
}
}
}
}
private static String annotationName(Annotation annotation) {
ITypeBinding binding= annotation.resolveTypeBinding();
return binding == null ? annotation.getTypeName().getFullyQualifiedName()
: binding.getQualifiedName();
}
private static String simpleName(String qualifiedName) {
int separator= qualifiedName.lastIndexOf('.');
return separator < 0 ? qualifiedName : qualifiedName.substring(separator + 1);
}
}