StructuredRewriteAnnotationGuard.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 org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.sandbox.jdt.triggerpattern.api.StructuredRewriteAction;
/**
* Fail-closed identity check for qualified annotation removals.
*
* <p>The standard action accepts simple annotation names for deliberately
* source-oriented rules. A qualified action argument, however, is an exact
* semantic contract and must never remove another annotation that merely has
* the same simple name.</p>
*/
final class StructuredRewriteAnnotationGuard {
enum AnnotationMatch {
EXACT,
COLLISION,
DIFFERENT
}
private StructuredRewriteAnnotationGuard() {
}
static void validate(StructuredRewriteAction action,
StructuredRewriteActionContext context) throws CoreException {
if (!"removeAnnotation".equals(action.name())) { //$NON-NLS-1$
return;
}
String expected= context.resolveString(action.requiredArgument("annotation")); //$NON-NLS-1$
if (expected.indexOf('.') < 0) {
return;
}
BodyDeclaration declaration= declarationTarget(context, action);
for (Object modifier : declaration.modifiers()) {
if (modifier instanceof Annotation annotation) {
AnnotationMatch match= classifyAnnotation(annotation, expected);
if (match == AnnotationMatch.EXACT) {
return;
}
if (match == AnnotationMatch.COLLISION) {
throw context.failure("Refusing to remove annotation " + expected //$NON-NLS-1$
+ " because a different annotation with the same simple name is selected"); //$NON-NLS-1$
}
}
}
throw context.failure("Structured action cannot find annotation " + expected); //$NON-NLS-1$
}
private static BodyDeclaration declarationTarget(StructuredRewriteActionContext context,
StructuredRewriteAction action) throws CoreException {
ASTNode current= context.resolveAuthorizedNode(action.requiredArgument("target")); //$NON-NLS-1$
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 AnnotationMatch classifyAnnotation(Annotation annotation, String expected) {
ITypeBinding binding= annotation.resolveTypeBinding();
String resolvedName= binding == null ? null : binding.getQualifiedName();
return classifyAnnotationName(expected, annotation.getTypeName().getFullyQualifiedName(),
resolvedName);
}
static AnnotationMatch classifyAnnotationName(String expected, String sourceName,
String resolvedName) {
if (expected == null || expected.isBlank() || sourceName == null || sourceName.isBlank()) {
return AnnotationMatch.DIFFERENT;
}
String actual= resolvedName == null || resolvedName.isBlank() ? sourceName : resolvedName;
if (expected.equals(actual)) {
return AnnotationMatch.EXACT;
}
return simpleName(expected).equals(simpleName(actual))
? AnnotationMatch.COLLISION : AnnotationMatch.DIFFERENT;
}
private static String simpleName(String qualifiedName) {
int separator= qualifiedName.lastIndexOf('.');
return separator < 0 ? qualifiedName : qualifiedName.substring(separator + 1);
}
}