BuiltInGuards.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 - initial API and implementation
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.internal;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.CharacterLiteral;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.Expression;
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.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.NullLiteral;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.PackageDeclaration;
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.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.sandbox.jdt.triggerpattern.api.GuardContext;
import org.sandbox.jdt.triggerpattern.api.GuardFunction;
import org.sandbox.jdt.triggerpattern.nullability.NullabilityGuard;
import org.sandbox.jdt.triggerpattern.nullability.NullabilityResult;
import org.sandbox.jdt.triggerpattern.nullability.NullStatus;
/**
* Built-in guard function implementations for the trigger pattern engine.
*
* <p>This utility class contains all built-in guard functions extracted from
* {@code GuardRegistry}. It has no OSGi or Eclipse runtime dependencies and
* uses only {@code org.eclipse.jdt.core.dom.*} and pure Java.</p>
*
* <p>Call {@link #registerAll(Map)} to populate a guard map with all built-in
* guard functions.</p>
*
* @since 1.3.2
*/
public final class BuiltInGuards {
/** Cached NullabilityGuard instance to avoid repeated initialization. */
private static final NullabilityGuard NULLABILITY_GUARD = new NullabilityGuard();
private BuiltInGuards() {
// utility class
}
/**
* Registers all built-in guard functions into the provided map.
*
* @param guards the map to populate with guard function entries
*/
public static void registerAll(Map<String, GuardFunction> guards) {
// Type guards
guards.put("instanceof", BuiltInGuards::evaluateInstanceOf); //$NON-NLS-1$
guards.put("subtypeOf", BuiltInGuards::evaluateSubtypeOf); //$NON-NLS-1$
// Structural guards
guards.put("matchesAny", BuiltInGuards::evaluateMatchesAny); //$NON-NLS-1$
guards.put("matchesNone", BuiltInGuards::evaluateMatchesNone); //$NON-NLS-1$
guards.put("hasNoSideEffect", BuiltInGuards::evaluateHasNoSideEffect); //$NON-NLS-1$
guards.put("referencedIn", BuiltInGuards::evaluateReferencedIn); //$NON-NLS-1$
// Java version guards
guards.put("sourceVersionGE", BuiltInGuards::evaluateSourceVersionGE); //$NON-NLS-1$
guards.put("sourceVersionLE", BuiltInGuards::evaluateSourceVersionLE); //$NON-NLS-1$
guards.put("sourceVersionBetween", BuiltInGuards::evaluateSourceVersionBetween); //$NON-NLS-1$
// Element kind guards
guards.put("isStatic", BuiltInGuards::evaluateIsStatic); //$NON-NLS-1$
guards.put("isFinal", BuiltInGuards::evaluateIsFinal); //$NON-NLS-1$
guards.put("elementKindMatches", BuiltInGuards::evaluateElementKindMatches); //$NON-NLS-1$
// Annotation guards
guards.put("hasAnnotation", BuiltInGuards::evaluateHasAnnotation); //$NON-NLS-1$
guards.put("isDeprecated", BuiltInGuards::evaluateIsDeprecated); //$NON-NLS-1$
// Negated pattern guards
guards.put("contains", BuiltInGuards::evaluateContains); //$NON-NLS-1$
guards.put("notContains", BuiltInGuards::evaluateNotContains); //$NON-NLS-1$
// Nullability guards
guards.put("isNullable", BuiltInGuards::evaluateIsNullable); //$NON-NLS-1$
guards.put("isNonNull", BuiltInGuards::evaluateIsNonNull); //$NON-NLS-1$
// NetBeans compatibility: otherwise guard (always true)
guards.put("otherwise", (ctx, args) -> true); //$NON-NLS-1$
// Literal and type guards
guards.put("isLiteral", BuiltInGuards::evaluateIsLiteral); //$NON-NLS-1$
guards.put("isNullLiteral", BuiltInGuards::evaluateIsNullLiteral); //$NON-NLS-1$
guards.put("isCharsetString", BuiltInGuards::evaluateIsCharsetString); //$NON-NLS-1$
guards.put("isSingleCharacter", BuiltInGuards::evaluateIsSingleCharacter); //$NON-NLS-1$
guards.put("isRegexp", BuiltInGuards::evaluateIsRegexp); //$NON-NLS-1$
// Context guards
guards.put("isInTryWithResourceBlock", BuiltInGuards::evaluateIsInTryWithResourceBlock); //$NON-NLS-1$
guards.put("isPassedToMethod", BuiltInGuards::evaluateIsPassedToMethod); //$NON-NLS-1$
guards.put("inSerializableClass", BuiltInGuards::evaluateInSerializableClass); //$NON-NLS-1$
guards.put("containsAnnotation", BuiltInGuards::evaluateContainsAnnotation); //$NON-NLS-1$
guards.put("parentMatches", BuiltInGuards::evaluateParentMatches); //$NON-NLS-1$
// Scope guards
guards.put("inClass", BuiltInGuards::evaluateInClass); //$NON-NLS-1$
guards.put("inPackage", BuiltInGuards::evaluateInPackage); //$NON-NLS-1$
// Modifier guard
guards.put("hasModifier", BuiltInGuards::evaluateHasModifier); //$NON-NLS-1$
// Cleanup mode guard — checks sandbox.cleanup.mode compiler option
guards.put("mode", BuiltInGuards::evaluateMode); //$NON-NLS-1$
// Method name pattern guard — checks if method name matches a regex pattern
guards.put("methodNameMatches", BuiltInGuards::evaluateMethodNameMatches); //$NON-NLS-1$
// Type hierarchy guard — checks if enclosing class extends a given type
guards.put("enclosingClassExtends", BuiltInGuards::evaluateEnclosingClassExtends); //$NON-NLS-1$
// SuppressWarnings guard — checks if enclosing element has @SuppressWarnings with given key
guards.put("hasSuppressWarnings", BuiltInGuards::evaluateHasSuppressWarnings); //$NON-NLS-1$
// Field guard — checks if enclosing class has a field with a given name
guards.put("hasField", BuiltInGuards::evaluateHasField); //$NON-NLS-1$
// Loop guard — checks if the matched node is inside a loop
guards.put("isInLoop", BuiltInGuards::evaluateIsInLoop); //$NON-NLS-1$
// Parameter count guard — checks if enclosing method has expected param count
guards.put("paramCount", BuiltInGuards::evaluateParamCount); //$NON-NLS-1$
// Return type guard — checks if enclosing method's return type matches
guards.put("hasReturnType", BuiltInGuards::evaluateHasReturnType); //$NON-NLS-1$
// String literal guard — checks if a placeholder is a StringLiteral node
guards.put("isStringLiteral", BuiltInGuards::evaluateIsStringLiteral); //$NON-NLS-1$
// Access modifier guards — check visibility of enclosing declaration
guards.put("isPublic", BuiltInGuards::evaluateIsPublic); //$NON-NLS-1$
guards.put("isPrivate", BuiltInGuards::evaluateIsPrivate); //$NON-NLS-1$
guards.put("isProtected", BuiltInGuards::evaluateIsProtected); //$NON-NLS-1$
// Throws guard — checks if enclosing method declares a throws clause
guards.put("throwsException", BuiltInGuards::evaluateThrowsException); //$NON-NLS-1$
// Variable kind guards — check if a binding is a parameter or field
guards.put("isParameter", BuiltInGuards::evaluateIsParameter); //$NON-NLS-1$
guards.put("isField", BuiltInGuards::evaluateIsField); //$NON-NLS-1$
guards.put("isLocalVariable", BuiltInGuards::evaluateIsLocalVariable); //$NON-NLS-1$
// Assignment context guards — check if matched expression is in a local variable initializer
guards.put("isAssignedToLocalVariable", BuiltInGuards::evaluateIsAssignedToLocalVariable); //$NON-NLS-1$
// Type hierarchy guard — checks if a variable's type implements AutoCloseable
guards.put("isAutoCloseable", BuiltInGuards::evaluateIsAutoCloseable); //$NON-NLS-1$
// Constructor context guard — checks if matched node is inside a constructor
guards.put("isInConstructor", BuiltInGuards::evaluateIsInConstructor); //$NON-NLS-1$
// Method override guard — checks if enclosing class overrides a given method
guards.put("classOverrides", BuiltInGuards::evaluateClassOverrides); //$NON-NLS-1$
// Generic type parameter guard — checks if a binding's generic type parameter matches
guards.put("genericTypeIs", BuiltInGuards::evaluateGenericTypeIs); //$NON-NLS-1$
// Variadic argument count guard — checks number of matched variadic args
guards.put("argsCount", BuiltInGuards::evaluateArgsCount); //$NON-NLS-1$
// Resource variable guard — checks if a variable declaration is a resource
// that could be wrapped in try-with-resources
guards.put("isResourceVariable", BuiltInGuards::evaluateIsResourceVariable); //$NON-NLS-1$
}
/**
* Checks if the bound node's type matches a given type name via ITypeBinding.
* Supports array types (e.g., {@code Type[]}).
*
* <p><b>Graceful degradation:</b> If the type binding cannot be resolved
* (e.g., because {@code ASTParser.setResolveBindings(false)} was used),
* this guard returns {@code true} to allow the rule to match.
* This ensures that rules with type guards still work in environments
* where binding resolution is not available, though disambiguation
* between ambiguous rules will not be possible.</p>
*
* Args: [placeholderName, typeName]
*/
private static boolean evaluateInstanceOf(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String placeholderName = args[0].toString();
String typeName = args[1].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
ITypeBinding typeBinding = resolveTypeBinding(node);
if (typeBinding == null) {
// Graceful degradation: if bindings are not available,
// assume the guard matches (allows the rule to apply).
return true;
}
// Handle array types: "Type[]"
if (typeName.endsWith("[]") && typeName.length() > 2) { //$NON-NLS-1$
if (!typeBinding.isArray()) {
return false;
}
String elementTypeName = typeName.substring(0, typeName.length() - 2);
return matchesTypeName(typeBinding.getElementType(), elementTypeName);
}
return matchesTypeName(typeBinding, typeName);
}
/**
* Checks if the bound node's type is a subtype of a given fully qualified type name.
* Walks the type hierarchy via {@link ITypeBinding#getSuperclass()} and
* {@link ITypeBinding#getInterfaces()}.
*
* <p><b>Graceful degradation:</b> If the type binding cannot be resolved,
* this guard returns {@code true} to allow the rule to match.</p>
*
* Args: [placeholderName, fullyQualifiedTypeName]
* @since 1.4.0
*/
private static boolean evaluateSubtypeOf(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String placeholderName = args[0].toString();
String targetFqn = stripQuotes(args[1].toString());
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
ITypeBinding typeBinding = resolveTypeBinding(node);
if (typeBinding == null) {
// Graceful degradation: assume match if bindings not available
return true;
}
return isSubtypeOf(typeBinding, targetFqn, new java.util.HashSet<>());
}
/**
* Recursively checks if a type binding is a subtype of the given FQN,
* walking the superclass chain and all interfaces.
*/
private static boolean isSubtypeOf(ITypeBinding typeBinding, String targetFqn, java.util.Set<String> visited) {
if (typeBinding == null || typeBinding.isRecovered()) {
return false;
}
String qualifiedName = typeBinding.getQualifiedName();
if (!visited.add(qualifiedName)) {
return false; // Already visited — break potential cycle
}
if (targetFqn.equals(qualifiedName)) {
return true;
}
// Check superclass
if (isSubtypeOf(typeBinding.getSuperclass(), targetFqn, visited)) {
return true;
}
// Check interfaces
for (ITypeBinding iface : typeBinding.getInterfaces()) {
if (isSubtypeOf(iface, targetFqn, visited)) {
return true;
}
}
return false;
}
/**
* Returns true if a placeholder's text matches any of the given literals.
* With a single argument, returns true if the binding exists and is non-null.
* With multiple arguments, checks if the placeholder's value matches any literal.
*
* <p>For literal AST nodes (StringLiteral, NumberLiteral, CharacterLiteral, BooleanLiteral),
* the literal value is extracted for comparison. For other nodes, the source text is used.</p>
*
* Args: [placeholderName] or [placeholderName, literal1, literal2, ...]
*/
private static boolean evaluateMatchesAny(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
// If only placeholder name given, check existence
if (args.length == 1) {
if (node != null) {
return true;
}
List<ASTNode> listBinding = ctx.getListBinding(placeholderName);
return !listBinding.isEmpty();
}
// Multiple arguments: check if node value matches any literal
if (node == null) {
return false;
}
String nodeText = extractNodeText(node);
for (int i = 1; i < args.length; i++) {
String literal = stripQuotes(args[i].toString());
if (nodeText.equals(literal)) {
return true;
}
}
return false;
}
/**
* Returns true if a placeholder's text matches none of the given literals.
* With a single argument, returns true if the binding does not exist.
* With multiple arguments, checks that the placeholder's value matches no literal.
*
* <p>For literal AST nodes (StringLiteral, NumberLiteral, CharacterLiteral, BooleanLiteral),
* the literal value is extracted for comparison. For other nodes, the source text is used.</p>
*
* Args: [placeholderName] or [placeholderName, literal1, literal2, ...]
*/
private static boolean evaluateMatchesNone(GuardContext ctx, Object... args) {
if (args.length < 1) {
return true;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
// If only placeholder name given, check non-existence
if (args.length == 1) {
if (node != null) {
return false;
}
List<ASTNode> listBinding = ctx.getListBinding(placeholderName);
return listBinding.isEmpty();
}
// Multiple arguments: check that node value matches none of the literals
if (node == null) {
return true;
}
String nodeText = extractNodeText(node);
for (int i = 1; i < args.length; i++) {
String literal = stripQuotes(args[i].toString());
if (nodeText.equals(literal)) {
return false;
}
}
return true;
}
/**
* Checks if an expression has no side effects.
* Currently checks that the node is not a method invocation (conservative check).
* Args: [placeholderName]
*/
private static boolean evaluateHasNoSideEffect(GuardContext ctx, Object... args) {
if (args.length < 1) {
return true;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return true;
}
// Conservative: method invocations may have side effects
return !(node instanceof MethodInvocation);
}
/**
* Checks if the source version is greater than or equal to a given version.
* Args: [version]
*/
private static boolean evaluateSourceVersionGE(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
double requiredVersion = parseVersion(args[0].toString());
double sourceVersion = parseVersion(ctx.getSourceVersion());
return sourceVersion >= requiredVersion;
}
/**
* Checks if the source version is less than or equal to a given version.
* Args: [version]
*/
private static boolean evaluateSourceVersionLE(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
double requiredVersion = parseVersion(args[0].toString());
double sourceVersion = parseVersion(ctx.getSourceVersion());
return sourceVersion <= requiredVersion;
}
/**
* Checks if the source version is within a given range (inclusive).
* Args: [minVersion, maxVersion]
*/
private static boolean evaluateSourceVersionBetween(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
double minVersion = parseVersion(args[0].toString());
double maxVersion = parseVersion(args[1].toString());
double sourceVersion = parseVersion(ctx.getSourceVersion());
return sourceVersion >= minVersion && sourceVersion <= maxVersion;
}
/**
* Checks if a binding has the static modifier.
* Args: [placeholderName]
*/
private static boolean evaluateIsStatic(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return Modifier.isStatic(modifiers);
}
/**
* Checks if a binding has the final modifier.
* Args: [placeholderName]
*/
private static boolean evaluateIsFinal(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return Modifier.isFinal(modifiers);
}
/**
* Checks if a binding has a specific annotation.
* Args: [placeholderName, annotationName]
*/
@SuppressWarnings("unchecked")
private static boolean evaluateHasAnnotation(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String placeholderName = args[0].toString();
String annotationName = args[1].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
// Navigate to the enclosing body declaration to check annotations
BodyDeclaration bodyDecl = findEnclosingBodyDeclaration(node);
if (bodyDecl != null) {
for (Object modifier : bodyDecl.modifiers()) {
if (modifier instanceof Annotation annotation) {
String name = getAnnotationName(annotation);
if (annotationName.equals(name)) {
return true;
}
}
}
}
return false;
}
/**
* Checks if a binding is deprecated (has @Deprecated annotation or Javadoc @deprecated tag).
* Args: [placeholderName]
*/
private static boolean evaluateIsDeprecated(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
// Check via binding
IBinding binding = resolveBinding(node);
if (binding != null) {
return binding.isDeprecated();
}
return false;
}
/**
* Checks if variable {@code $x} is referenced within the AST subtree bound to {@code $y}.
* Args: [variablePlaceholderName, expressionPlaceholderName]
*/
private static boolean evaluateReferencedIn(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String varName = args[0].toString();
String exprName = args[1].toString();
ASTNode varNode = ctx.getBinding(varName);
ASTNode exprNode = ctx.getBinding(exprName);
if (varNode == null || exprNode == null) {
return false;
}
// Get the identifier text of the variable
String varIdentifier;
if (varNode instanceof SimpleName simpleName) {
varIdentifier = simpleName.getIdentifier();
} else {
varIdentifier = varNode.toString().trim();
}
// Walk the expression subtree looking for a SimpleName with the same identifier
boolean[] found = { false };
exprNode.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName name) {
if (name.getIdentifier().equals(varIdentifier)) {
found[0] = true;
}
return !found[0]; // stop visiting once found
}
});
return found[0];
}
/**
* Checks if a binding is of a specific element kind.
* Supported kinds: FIELD, METHOD, LOCAL_VARIABLE, PARAMETER, TYPE.
* Args: [placeholderName, elementKind]
*/
private static boolean evaluateElementKindMatches(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String placeholderName = args[0].toString();
String elementKind = args[1].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
return matchesElementKind(node, elementKind);
}
/**
* Checks if a text pattern occurs within the enclosing method body.
*
* <p>This guard traverses the enclosing method's body looking for a simple text
* match in the source representation. Useful for checking whether a particular
* call (e.g., {@code close()}) exists in the same method.</p>
*
* Args: [textToFind] or [placeholderName, textToFind]
*/
private static boolean evaluateContains(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String textToFind;
ASTNode contextNode;
if (args.length >= 2) {
// contains($x, "text") - search in $x's enclosing method
String placeholderName = args[0].toString();
textToFind = stripQuotes(args[1].toString());
contextNode = ctx.getBinding(placeholderName);
} else {
// contains("text") - search in matched node's enclosing method
textToFind = stripQuotes(args[0].toString());
contextNode = ctx.getMatchedNode();
}
if (contextNode == null) {
return false;
}
Block methodBody = findEnclosingMethodBody(contextNode);
if (methodBody == null) {
return false;
}
return methodBody.toString().contains(textToFind);
}
/**
* Checks if a text pattern does NOT occur within the enclosing method body.
*
* <p>Negation of {@link #evaluateContains}. Useful for detecting "missing calls"
* patterns (e.g., "close() is missing after open()").</p>
*
* Args: [textToFind] or [placeholderName, textToFind]
*/
private static boolean evaluateNotContains(GuardContext ctx, Object... args) {
return !evaluateContains(ctx, args);
}
/**
* Checks if a placeholder's expression is potentially nullable.
*
* <p>With one argument: returns true if the expression is not provably NON_NULL.</p>
* <p>With two arguments: computes a nullability score (0-10) and returns true
* only if score >= minScore. Score mapping:</p>
* <ul>
* <li>NON_NULL → 0 (definitely safe, no change needed)</li>
* <li>UNKNOWN → 5 (undetermined)</li>
* <li>POTENTIALLY_NULLABLE → 7 (there are null-checks nearby)</li>
* <li>NULLABLE → 10 (high risk, SpotBugs-style: null-check found after usage)</li>
* </ul>
*
* Args: [placeholderName] or [placeholderName, minScore]
*/
private static boolean evaluateIsNullable(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
// Cast to Expression, return false if not
if (!(node instanceof Expression expression)) {
return false;
}
// Analyze nullability
NullabilityResult result = NULLABILITY_GUARD.analyze(expression);
// Map NullStatus to score
int score = mapNullStatusToScore(result.status());
// With one argument: return true if NOT NON_NULL
if (args.length == 1) {
return result.status() != NullStatus.NON_NULL;
}
// With two arguments: compare score with minScore
try {
int minScore = Integer.parseInt(args[1].toString());
return score >= minScore;
} catch (NumberFormatException e) {
// Invalid minScore, fall back to single-argument behavior
return result.status() != NullStatus.NON_NULL;
}
}
/**
* Checks if a placeholder's expression is provably non-null.
*
* <p>Returns true if the {@code NullabilityGuard.analyze()} determines
* the expression is {@code NON_NULL}.</p>
*
* Args: [placeholderName]
*/
private static boolean evaluateIsNonNull(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
// Cast to Expression, return false if not
if (!(node instanceof Expression expression)) {
return false;
}
// Analyze nullability
NullabilityResult result = NULLABILITY_GUARD.analyze(expression);
return result.status() == NullStatus.NON_NULL;
}
/**
* Maps a NullStatus to a numeric score for comparison.
*
* @param status the null status
* @return score from 0 (definitely non-null) to 10 (definitely nullable)
*/
private static int mapNullStatusToScore(NullStatus status) {
return switch (status) {
case NON_NULL -> 0;
case UNKNOWN -> 5;
case POTENTIALLY_NULLABLE -> 7;
case NULLABLE -> 10;
};
}
// ---- New NetBeans-compatible guard implementations ----
/**
* Standard charsets supported by {@link java.nio.charset.StandardCharsets}.
*/
private static final Set<String> STANDARD_CHARSETS = Set.of(
"UTF-8", "UTF-16", "UTF-16BE", "UTF-16LE", "ISO-8859-1", "US-ASCII"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
/**
* Characters that indicate a regex pattern (not a plain literal).
*/
private static final String REGEX_META_CHARS = ".\\+*^$?|[](){}-"; //$NON-NLS-1$
/**
* Checks if the bound node is any literal AST node type.
* Args: [placeholderName]
*/
private static boolean evaluateIsLiteral(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
ASTNode node = ctx.getBinding(args[0].toString());
if (node == null) {
return false;
}
return node instanceof StringLiteral
|| node instanceof NumberLiteral
|| node instanceof CharacterLiteral
|| node instanceof BooleanLiteral
|| node instanceof NullLiteral;
}
/**
* Checks if the bound node is a {@link NullLiteral}.
* Args: [placeholderName]
*/
private static boolean evaluateIsNullLiteral(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
ASTNode node = ctx.getBinding(args[0].toString());
return node instanceof NullLiteral;
}
/**
* Checks if the bound node is a {@link StringLiteral} whose value is a
* standard charset name (UTF-8, UTF-16, UTF-16BE, UTF-16LE, ISO-8859-1, US-ASCII).
* Args: [placeholderName]
*/
private static boolean evaluateIsCharsetString(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
ASTNode node = ctx.getBinding(args[0].toString());
if (!(node instanceof StringLiteral stringLiteral)) {
return false;
}
return STANDARD_CHARSETS.contains(stringLiteral.getLiteralValue());
}
/**
* Checks if the bound node is a {@link StringLiteral} with exactly one character.
* Args: [placeholderName]
*/
private static boolean evaluateIsSingleCharacter(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
ASTNode node = ctx.getBinding(args[0].toString());
if (!(node instanceof StringLiteral stringLiteral)) {
return false;
}
return stringLiteral.getLiteralValue().length() == 1;
}
/**
* Checks if the bound node is a {@link StringLiteral} containing regex metacharacters.
* Args: [placeholderName]
*/
private static boolean evaluateIsRegexp(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
ASTNode node = ctx.getBinding(args[0].toString());
if (!(node instanceof StringLiteral stringLiteral)) {
return false;
}
String value = stringLiteral.getLiteralValue();
for (int i = 0; i < value.length(); i++) {
if (REGEX_META_CHARS.indexOf(value.charAt(i)) >= 0) {
return true;
}
}
return false;
}
/**
* Checks if the matched node is inside a try-with-resources block.
* Args: [] or [placeholderName]
*/
@SuppressWarnings("unchecked")
private static boolean evaluateIsInTryWithResourceBlock(GuardContext ctx, Object... args) {
ASTNode node;
if (args.length >= 1) {
node = ctx.getBinding(args[0].toString());
} else {
node = ctx.getMatchedNode();
}
if (node == null) {
return false;
}
ASTNode current = node.getParent();
while (current != null) {
if (current instanceof TryStatement tryStmt) {
List<Expression> resources = tryStmt.resources();
for (Expression resource : resources) {
if (isAncestorOrSelf(resource, node)) {
return true;
}
}
}
current = current.getParent();
}
return false;
}
/**
* Checks if the matched node is passed as an argument to a method invocation
* or constructor call.
* Args: [] or [placeholderName]
*/
@SuppressWarnings("unchecked")
private static boolean evaluateIsPassedToMethod(GuardContext ctx, Object... args) {
ASTNode node;
if (args.length >= 1) {
node = ctx.getBinding(args[0].toString());
} else {
node = ctx.getMatchedNode();
}
if (node == null) {
return false;
}
ASTNode parent = node.getParent();
if (parent instanceof MethodInvocation mi) {
return mi.arguments().contains(node);
}
if (parent instanceof ClassInstanceCreation cic) {
return cic.arguments().contains(node);
}
return false;
}
/**
* Checks if the matched node is inside a class that implements {@code java.io.Serializable}.
* Args: [] or [placeholderName]
*/
private static boolean evaluateInSerializableClass(GuardContext ctx, Object... args) {
ASTNode node;
if (args.length >= 1) {
node = ctx.getBinding(args[0].toString());
} else {
node = ctx.getMatchedNode();
}
if (node == null) {
return false;
}
TypeDeclaration typeDecl = findEnclosingTypeDeclaration(node);
if (typeDecl == null) {
return false;
}
ITypeBinding typeBinding = typeDecl.resolveBinding();
if (typeBinding == null) {
return false;
}
return implementsSerializable(typeBinding);
}
/**
* Checks if a modifiers list or body declaration contains a specific annotation.
* Args: [placeholderName, annotationFqn]
*/
@SuppressWarnings("unchecked")
private static boolean evaluateContainsAnnotation(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String placeholderName = args[0].toString();
String annotationName = stripQuotes(args[1].toString());
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
// If the node is a BodyDeclaration, check its modifiers
BodyDeclaration bodyDecl;
if (node instanceof BodyDeclaration bd) {
bodyDecl = bd;
} else {
bodyDecl = findEnclosingBodyDeclaration(node);
}
if (bodyDecl != null) {
for (Object modifier : bodyDecl.modifiers()) {
if (modifier instanceof Annotation annotation) {
String name = getAnnotationName(annotation);
if (annotationName.equals(name)) {
return true;
}
}
}
}
return false;
}
/**
* Checks if the parent of the matched node matches a given expression pattern string.
*
* <p><b>Limitations:</b> This performs a simple string-contains check on the
* parent's {@code toString()} representation. It may produce false positives
* for partial name matches. The {@code $_} placeholder in the pattern is
* stripped before matching. For more precise matching, consider using
* the pattern matching engine directly.</p>
*
* Args: [pattern] or [placeholderName, pattern]
*/
private static boolean evaluateParentMatches(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
ASTNode node;
String pattern;
if (args.length >= 2) {
node = ctx.getBinding(args[0].toString());
pattern = stripQuotes(args[1].toString());
} else {
node = ctx.getMatchedNode();
pattern = stripQuotes(args[0].toString());
}
if (node == null) {
return false;
}
ASTNode parent = node.getParent();
if (parent == null) {
return false;
}
// Simple string-contains check on the parent's source representation
// Replace $_ with .* for wildcard matching concept
String parentText = parent.toString().trim();
String simplePattern = pattern.replace("$_", ""); //$NON-NLS-1$ //$NON-NLS-2$
return parentText.contains(simplePattern);
}
/**
* Checks if the matched node is inside a class with the given fully qualified name.
* Args: [fqn]
*/
private static boolean evaluateInClass(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String classFqn = stripQuotes(args[0].toString());
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
TypeDeclaration typeDecl = findEnclosingTypeDeclaration(node);
if (typeDecl == null) {
return false;
}
ITypeBinding typeBinding = typeDecl.resolveBinding();
if (typeBinding != null) {
return classFqn.equals(typeBinding.getQualifiedName());
}
// Fallback: compare simple name
return classFqn.equals(typeDecl.getName().getIdentifier())
|| classFqn.endsWith("." + typeDecl.getName().getIdentifier()); //$NON-NLS-1$
}
/**
* Checks if the matched node is inside a package with the given name.
* Args: [packageName]
*/
private static boolean evaluateInPackage(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String packageName = stripQuotes(args[0].toString());
CompilationUnit cu = ctx.getCompilationUnit();
if (cu == null) {
return false;
}
PackageDeclaration packageDecl = cu.getPackage();
if (packageDecl == null) {
return packageName.isEmpty();
}
return packageName.equals(packageDecl.getName().getFullyQualifiedName());
}
/**
* Checks if a binding has a specific modifier.
* Supports: PUBLIC, PRIVATE, PROTECTED, ABSTRACT, STATIC, FINAL, SYNCHRONIZED, VOLATILE, TRANSIENT, NATIVE, STRICTFP.
* Args: [placeholderName, modifierName]
*/
private static boolean evaluateHasModifier(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String placeholderName = args[0].toString();
String modifierName = args[1].toString().toUpperCase(Locale.ROOT);
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return matchesModifierName(modifiers, modifierName);
}
// ---- Helper methods ----
/**
* Resolves the type binding for an AST node.
*/
private static ITypeBinding resolveTypeBinding(ASTNode node) {
if (node instanceof Name name) {
IBinding binding = name.resolveBinding();
if (binding instanceof IVariableBinding varBinding) {
return varBinding.getType();
}
if (binding instanceof ITypeBinding typeBinding) {
return typeBinding;
}
}
if (node instanceof MethodInvocation methodInv) {
IMethodBinding methodBinding = methodInv.resolveMethodBinding();
if (methodBinding != null) {
return methodBinding.getReturnType();
}
}
if (node instanceof Expression expr) {
return expr.resolveTypeBinding();
}
return null;
}
/**
* Checks if a type binding matches a given type name (simple or qualified).
*/
private static boolean matchesTypeName(ITypeBinding typeBinding, String typeName) {
if (typeBinding.getName().equals(typeName)) {
return true;
}
if (typeBinding.getQualifiedName().equals(typeName)) {
return true;
}
// Check supertypes
ITypeBinding superclass = typeBinding.getSuperclass();
if (superclass != null && matchesTypeName(superclass, typeName)) {
return true;
}
for (ITypeBinding iface : typeBinding.getInterfaces()) {
if (matchesTypeName(iface, typeName)) {
return true;
}
}
return false;
}
/**
* Resolves an IBinding from an AST node.
*/
private static IBinding resolveBinding(ASTNode node) {
if (node instanceof SimpleName simpleName) {
return simpleName.resolveBinding();
}
if (node instanceof MethodDeclaration methodDecl) {
return methodDecl.resolveBinding();
}
if (node instanceof VariableDeclarationFragment varFrag) {
return varFrag.resolveBinding();
}
if (node instanceof SingleVariableDeclaration singleVar) {
return singleVar.resolveBinding();
}
return null;
}
/**
* Resolves the modifiers for an AST node via its binding.
*
* <p>When binding resolution is unavailable (e.g., standalone ASTParser
* without a project), this method falls back to navigating from a
* {@link SimpleName} to its parent {@link BodyDeclaration} to read
* modifiers directly from the AST.</p>
*/
private static int resolveModifiers(ASTNode node) {
IBinding binding = resolveBinding(node);
if (binding != null) {
return binding.getModifiers();
}
if (node instanceof BodyDeclaration bodyDecl) {
return bodyDecl.getModifiers();
}
// Fallback: navigate from SimpleName to parent BodyDeclaration
// This handles METHOD_DECLARATION patterns where $name binds to the
// method's SimpleName but binding resolution is unavailable.
if (node instanceof SimpleName) {
ASTNode parent = node.getParent();
if (parent instanceof BodyDeclaration parentDecl) {
return parentDecl.getModifiers();
}
}
return 0;
}
/**
* Gets the simple name of an annotation.
*/
private static String getAnnotationName(Annotation annotation) {
if (annotation instanceof MarkerAnnotation marker) {
return marker.getTypeName().getFullyQualifiedName();
}
if (annotation instanceof SingleMemberAnnotation sma) {
return sma.getTypeName().getFullyQualifiedName();
}
if (annotation instanceof NormalAnnotation normal) {
return normal.getTypeName().getFullyQualifiedName();
}
return ""; //$NON-NLS-1$
}
/**
* Parses a Java source version string to a numeric value.
* Handles formats like "1.8", "11", "17", "21".
*
* <p>Old-style Java version strings like "1.5", "1.6", "1.7", "1.8" are
* converted to their major version equivalents (5, 6, 7, 8) so that
* guards like {@code sourceVersionGE(7)} work correctly for Java 8
* (source version "1.8").</p>
*/
private static double parseVersion(String version) {
if (version == null || version.isEmpty()) {
return 0;
}
try {
double v = Double.parseDouble(version);
// Convert old-style "1.x" versions (1.5, 1.6, 1.7, 1.8) to major versions (5, 6, 7, 8)
if (v > 1.0 && v < 2.0) {
return Math.round(v * 10) - 10; // 1.5→5, 1.6→6, 1.7→7, 1.8→8
}
return v;
} catch (NumberFormatException e) {
return 0;
}
}
/**
* Extracts a comparable text value from an AST node.
*
* <p>For literal nodes, extracts the literal value (without surrounding quotes).
* For other nodes, falls back to {@code toString().trim()}.</p>
*
* @param node the AST node to extract text from
* @return the extracted text value
*/
private static String extractNodeText(ASTNode node) {
if (node instanceof StringLiteral stringLiteral) {
return stringLiteral.getLiteralValue();
}
if (node instanceof CharacterLiteral charLiteral) {
return String.valueOf(charLiteral.charValue());
}
if (node instanceof NumberLiteral numberLiteral) {
return numberLiteral.getToken();
}
if (node instanceof BooleanLiteral boolLiteral) {
return String.valueOf(boolLiteral.booleanValue());
}
return node.toString().trim();
}
/**
* Strips surrounding quotes from a string literal argument.
*/
private static String stripQuotes(String value) {
if (value.length() >= 2
&& ((value.startsWith("\"") && value.endsWith("\"")) //$NON-NLS-1$ //$NON-NLS-2$
|| (value.startsWith("'") && value.endsWith("'")))) { //$NON-NLS-1$ //$NON-NLS-2$
return value.substring(1, value.length() - 1);
}
return value;
}
/**
* Finds the enclosing method body for an AST node.
*/
private static Block findEnclosingMethodBody(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof MethodDeclaration methodDecl) {
return methodDecl.getBody();
}
current = current.getParent();
}
return null;
}
/**
* Finds the nearest enclosing BodyDeclaration for an AST node.
*/
private static BodyDeclaration findEnclosingBodyDeclaration(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof BodyDeclaration bodyDecl) {
return bodyDecl;
}
current = current.getParent();
}
return null;
}
/**
* Checks if an AST node matches the given element kind string.
*/
private static boolean matchesElementKind(ASTNode node, String elementKind) {
// Try via binding first
IBinding binding = resolveBinding(node);
if (binding != null) {
return matchesBindingKind(binding, elementKind);
}
// Fallback: check AST node type directly
return matchesNodeKind(node, elementKind);
}
/**
* Matches a binding against an element kind string.
*/
private static boolean matchesBindingKind(IBinding binding, String elementKind) {
return switch (elementKind.toUpperCase(Locale.ROOT)) {
case "FIELD" -> binding instanceof IVariableBinding vb && vb.isField(); //$NON-NLS-1$
case "METHOD" -> binding instanceof IMethodBinding; //$NON-NLS-1$
case "LOCAL_VARIABLE" -> binding instanceof IVariableBinding vb && !vb.isField() && !vb.isParameter(); //$NON-NLS-1$
case "PARAMETER" -> binding instanceof IVariableBinding vb && vb.isParameter(); //$NON-NLS-1$
case "TYPE" -> binding instanceof ITypeBinding; //$NON-NLS-1$
default -> false;
};
}
/**
* Matches an AST node type against an element kind string (fallback when binding is unavailable).
*/
private static boolean matchesNodeKind(ASTNode node, String elementKind) {
return switch (elementKind.toUpperCase(Locale.ROOT)) {
case "FIELD" -> node instanceof FieldDeclaration //$NON-NLS-1$
|| (node instanceof VariableDeclarationFragment vdf && vdf.getParent() instanceof FieldDeclaration);
case "METHOD" -> node instanceof MethodDeclaration; //$NON-NLS-1$
case "LOCAL_VARIABLE" -> node instanceof VariableDeclarationStatement //$NON-NLS-1$
|| (node instanceof VariableDeclarationFragment vdf && vdf.getParent() instanceof VariableDeclarationStatement);
case "PARAMETER" -> node instanceof SingleVariableDeclaration; //$NON-NLS-1$
case "TYPE" -> node instanceof TypeDeclaration //$NON-NLS-1$
|| node instanceof EnumDeclaration
|| node instanceof RecordDeclaration;
default -> false;
};
}
/**
* Finds the nearest enclosing TypeDeclaration for an AST node.
*/
private static TypeDeclaration findEnclosingTypeDeclaration(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof TypeDeclaration typeDecl) {
return typeDecl;
}
current = current.getParent();
}
return null;
}
/**
* Checks if a type binding implements {@code java.io.Serializable} (directly or transitively).
* Uses a visited set to prevent infinite recursion in case of cycles in the type hierarchy.
*/
private static boolean implementsSerializable(ITypeBinding typeBinding) {
return implementsSerializable(typeBinding, new java.util.HashSet<>());
}
private static boolean implementsSerializable(ITypeBinding typeBinding, java.util.Set<String> visited) {
String qualifiedName = typeBinding.getQualifiedName();
if (!visited.add(qualifiedName)) {
return false; // Already visited — break potential cycle
}
for (ITypeBinding iface : typeBinding.getInterfaces()) {
if ("java.io.Serializable".equals(iface.getQualifiedName())) { //$NON-NLS-1$
return true;
}
if (implementsSerializable(iface, visited)) {
return true;
}
}
ITypeBinding superclass = typeBinding.getSuperclass();
if (superclass != null) {
return implementsSerializable(superclass, visited);
}
return false;
}
/**
* Checks if {@code ancestor} is an ancestor of {@code node} (or the same node).
*/
private static boolean isAncestorOrSelf(ASTNode ancestor, ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current == ancestor) {
return true;
}
current = current.getParent();
}
return false;
}
/**
* Checks if the given modifier flags contain the named modifier.
*/
private static boolean matchesModifierName(int modifiers, String modifierName) {
return switch (modifierName) {
case "PUBLIC" -> Modifier.isPublic(modifiers); //$NON-NLS-1$
case "PRIVATE" -> Modifier.isPrivate(modifiers); //$NON-NLS-1$
case "PROTECTED" -> Modifier.isProtected(modifiers); //$NON-NLS-1$
case "ABSTRACT" -> Modifier.isAbstract(modifiers); //$NON-NLS-1$
case "STATIC" -> Modifier.isStatic(modifiers); //$NON-NLS-1$
case "FINAL" -> Modifier.isFinal(modifiers); //$NON-NLS-1$
case "SYNCHRONIZED" -> Modifier.isSynchronized(modifiers); //$NON-NLS-1$
case "VOLATILE" -> Modifier.isVolatile(modifiers); //$NON-NLS-1$
case "TRANSIENT" -> Modifier.isTransient(modifiers); //$NON-NLS-1$
case "NATIVE" -> Modifier.isNative(modifiers); //$NON-NLS-1$
case "STRICTFP" -> Modifier.isStrict(modifiers); //$NON-NLS-1$
default -> false;
};
}
/**
* Checks if the cleanup mode matches a given mode name.
* The mode is passed via the {@code sandbox.cleanup.mode} compiler option.
*
* <p>This guard enables mode-dependent DSL rules, e.g.:</p>
* <pre>
* $s.getBytes("${CHARSET}") :: sourceVersionGE(7), mode(ENFORCE_UTF8)
* => $s.getBytes(java.nio.charset.StandardCharsets.${CHARSET_CONSTANT})
* ;;
* </pre>
*
* <p>Supported modes: {@code KEEP_BEHAVIOR}, {@code ENFORCE_UTF8},
* {@code ENFORCE_UTF8_AGGREGATE}.</p>
*
* Args: [modeName]
*/
private static boolean evaluateMode(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String requiredMode = args[0].toString().trim();
String currentMode = ctx.getCompilerOptions().get("sandbox.cleanup.mode"); //$NON-NLS-1$
if (currentMode == null) {
return false;
}
return requiredMode.equalsIgnoreCase(currentMode.trim());
}
/**
* Checks if a method name (bound to a placeholder) matches a given regex pattern.
*
* <p>This guard is typically used with {@code METHOD_DECLARATION} patterns to
* filter methods by name. The placeholder must be bound to a {@link SimpleName}
* (the method name).</p>
*
* <p>Example DSL usage:</p>
* <pre>
* void $name($params$) :: methodNameMatches($name, "test.*")
* => addAnnotation @org.junit.jupiter.api.Test
* ;;
* </pre>
*
* Args: [placeholderName, regexPattern]
* @since 1.3.9
*/
private static boolean evaluateMethodNameMatches(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String placeholderName = args[0].toString();
String regexPattern = stripQuotes(args[1].toString());
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
String methodName;
if (node instanceof SimpleName simpleName) {
methodName = simpleName.getIdentifier();
} else if (node instanceof MethodDeclaration methodDecl) {
methodName = methodDecl.getName().getIdentifier();
} else {
methodName = node.toString().trim();
}
try {
return methodName.matches(regexPattern);
} catch (java.util.regex.PatternSyntaxException e) {
return false;
}
}
/**
* Checks if the enclosing class extends a given type (directly or transitively).
*
* <p>This guard walks the superclass chain of the enclosing class to determine
* if it extends the specified type. This is essential for migration rules that
* should only apply to classes inheriting from a specific base class (e.g.,
* JUnit 3 test classes extending {@code junit.framework.TestCase}).</p>
*
* <p><b>Graceful degradation:</b> If type bindings cannot be resolved, falls
* back to a textual comparison of the {@code extends} clause's simple name
* against the last segment of the given FQN. This provides partial matching
* in environments where binding resolution is not available, but cannot detect
* transitive inheritance without bindings.</p>
*
* <p>Example DSL usage:</p>
* <pre>
* void $name($params$) :: methodNameMatches($name, "test.*") && enclosingClassExtends("junit.framework.TestCase")
* => @org.junit.jupiter.api.Test void $name($params$)
* ;;
* </pre>
*
* Args: [fullyQualifiedTypeName]
* @since 1.3.10
*/
private static boolean evaluateEnclosingClassExtends(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String targetFqn = stripQuotes(args[0].toString());
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
TypeDeclaration typeDecl = findEnclosingTypeDeclaration(node);
if (typeDecl == null) {
return false;
}
ITypeBinding typeBinding = typeDecl.resolveBinding();
if (typeBinding != null && !typeBinding.isRecovered()) {
ITypeBinding superclass = typeBinding.getSuperclass();
if (superclass != null && !superclass.isRecovered()) {
return extendsType(superclass, targetFqn, new java.util.HashSet<>());
}
}
// Fallback without reliable bindings: check the extends clause textually (direct superclass only)
org.eclipse.jdt.core.dom.Type superclassType = typeDecl.getSuperclassType();
if (superclassType == null) {
return false;
}
String superclassText = superclassType.toString().trim();
// Match if the extends clause is the FQN or the simple name part of it
int lastDot = targetFqn.lastIndexOf('.');
String simpleName = (lastDot >= 0) ? targetFqn.substring(lastDot + 1) : targetFqn;
return targetFqn.equals(superclassText) || simpleName.equals(superclassText);
}
/**
* Walks the superclass chain of a type binding to check if it extends the target type.
* Uses a visited set to prevent infinite recursion in case of cycles.
* Stops at recovered bindings since they are unreliable.
*/
private static boolean extendsType(ITypeBinding typeBinding, String targetFqn, java.util.Set<String> visited) {
if (typeBinding == null || typeBinding.isRecovered()) {
return false;
}
String qualifiedName = typeBinding.getQualifiedName();
if (!visited.add(qualifiedName)) {
return false; // Already visited — break potential cycle
}
if (targetFqn.equals(qualifiedName)) {
return true;
}
ITypeBinding superclass = typeBinding.getSuperclass();
return extendsType(superclass, targetFqn, visited);
}
/**
* Checks if the matched node's enclosing method, field, or type declaration has a
* {@code @SuppressWarnings} annotation containing the specified key.
*
* <p>Walks up the AST from the matched node checking each enclosing
* {@link BodyDeclaration} for a {@code @SuppressWarnings} annotation
* whose value contains the given key.</p>
*
* Args: [suppressWarningsKey]
* @since 1.4.0
*/
private static boolean evaluateHasSuppressWarnings(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String key = stripQuotes(args[0].toString());
ASTNode node = ctx.getMatchedNode();
return SuppressWarningsChecker.isSuppressed(node, key);
}
/**
* Checks if the enclosing class has a field with the given name.
*
* <p>Walks to the enclosing {@link TypeDeclaration} and iterates its
* {@code bodyDeclarations()} looking for a {@link FieldDeclaration}
* containing a {@link VariableDeclarationFragment} with the matching name.</p>
*
* Args: [fieldName]
* @since 1.4.1
*/
@SuppressWarnings("unchecked")
private static boolean evaluateHasField(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String fieldName = stripQuotes(args[0].toString());
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
TypeDeclaration typeDecl = findEnclosingTypeDeclaration(node);
if (typeDecl == null) {
return false;
}
for (Object bodyDecl : typeDecl.bodyDeclarations()) {
if (bodyDecl instanceof FieldDeclaration fieldDecl) {
for (Object frag : fieldDecl.fragments()) {
if (frag instanceof VariableDeclarationFragment vdf
&& fieldName.equals(vdf.getName().getIdentifier())) {
return true;
}
}
}
}
return false;
}
/**
* Checks if the matched node is inside a loop construct.
*
* <p>Walks up from the matched node checking if any parent is a
* {@code ForStatement}, {@code WhileStatement}, {@code DoStatement},
* or {@code EnhancedForStatement}.</p>
*
* Args: none
* @since 1.4.1
*/
private static boolean evaluateIsInLoop(GuardContext ctx, Object... args) {
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
ASTNode current = node.getParent();
while (current != null) {
int nodeType = current.getNodeType();
if (nodeType == ASTNode.FOR_STATEMENT
|| nodeType == ASTNode.WHILE_STATEMENT
|| nodeType == ASTNode.DO_STATEMENT
|| nodeType == ASTNode.ENHANCED_FOR_STATEMENT) {
return true;
}
current = current.getParent();
}
return false;
}
/**
* Checks if the enclosing method's parameter count matches the expected value.
*
* <p>Finds the enclosing {@link MethodDeclaration} and compares its
* parameter count with the expected value.</p>
*
* Args: [expectedCount]
* @since 1.4.1
*/
private static boolean evaluateParamCount(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
int expectedCount;
try {
expectedCount = Integer.parseInt(args[0].toString().trim());
} catch (NumberFormatException e) {
return false;
}
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
MethodDeclaration methodDecl = findEnclosingMethodDeclaration(node);
if (methodDecl == null) {
return false;
}
return methodDecl.parameters().size() == expectedCount;
}
/**
* Checks if the enclosing method's return type matches the given type name.
*
* <p>Expects a single argument, the type name to compare with the return
* type of the enclosing {@link MethodDeclaration}.</p>
*
* Args: [typeName]
* @since 1.4.1
*/
private static boolean evaluateHasReturnType(GuardContext ctx, Object... args) {
if (args.length != 1) {
return false;
}
String typeName = stripQuotes(args[0].toString());
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
MethodDeclaration methodDecl = findEnclosingMethodDeclaration(node);
if (methodDecl == null) {
return false;
}
org.eclipse.jdt.core.dom.Type returnType = methodDecl.getReturnType2();
if (returnType == null) {
return "void".equals(typeName); //$NON-NLS-1$
}
String returnTypeStr = returnType.toString().trim();
return typeName.equals(returnTypeStr);
}
/**
* Checks if the bound placeholder is a {@link StringLiteral} node.
*
* Args: [placeholderName]
* @since 1.4.1
*/
private static boolean evaluateIsStringLiteral(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
ASTNode node = ctx.getBinding(args[0].toString());
return node instanceof StringLiteral;
}
/**
* Finds the nearest enclosing MethodDeclaration for an AST node.
*/
private static MethodDeclaration findEnclosingMethodDeclaration(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof MethodDeclaration methodDecl) {
return methodDecl;
}
current = current.getParent();
}
return null;
}
/**
* Checks if a binding has the public modifier.
* Args: [placeholderName]
* @since 1.4.1
*/
private static boolean evaluateIsPublic(GuardContext ctx, Object... args) {
if (args.length < 1) {
// No argument: check the matched node directly
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return Modifier.isPublic(modifiers);
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return Modifier.isPublic(modifiers);
}
/**
* Checks if a binding has the private modifier.
* Args: [placeholderName]
* @since 1.4.1
*/
private static boolean evaluateIsPrivate(GuardContext ctx, Object... args) {
if (args.length < 1) {
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return Modifier.isPrivate(modifiers);
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return Modifier.isPrivate(modifiers);
}
/**
* Checks if a binding has the protected modifier.
* Args: [placeholderName]
* @since 1.4.1
*/
private static boolean evaluateIsProtected(GuardContext ctx, Object... args) {
if (args.length < 1) {
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return Modifier.isProtected(modifiers);
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
int modifiers = resolveModifiers(node);
return Modifier.isProtected(modifiers);
}
/**
* Checks if the enclosing method declares a throws clause matching the given type.
*
* <p>If no argument is given, returns true if the method has any throws clause.
* If a type name argument is given, checks if the method throws that specific type.</p>
*
* Args: [] (any throws) or [exceptionTypeName]
* @since 1.4.1
*/
@SuppressWarnings("unchecked")
private static boolean evaluateThrowsException(GuardContext ctx, Object... args) {
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
MethodDeclaration methodDecl = findEnclosingMethodDeclaration(node);
if (methodDecl == null) {
return false;
}
List<Name> thrownExceptions = methodDecl.thrownExceptionTypes();
if (thrownExceptions.isEmpty()) {
return false;
}
if (args.length < 1) {
return true; // any throws clause present
}
String targetType = stripQuotes(args[0].toString());
for (Object exType : thrownExceptions) {
if (exType instanceof Name name) {
if (targetType.equals(name.getFullyQualifiedName())) {
return true;
}
// Also check simple name match
String simpleName = name.getFullyQualifiedName();
int lastDot = simpleName.lastIndexOf('.');
if (lastDot >= 0) {
simpleName = simpleName.substring(lastDot + 1);
}
if (targetType.equals(simpleName)) {
return true;
}
} else if (exType instanceof org.eclipse.jdt.core.dom.Type type) {
String typeName = type.toString();
if (targetType.equals(typeName)) {
return true;
}
}
}
return false;
}
/**
* Checks if a binding is a method parameter.
*
* <p>Uses {@link IVariableBinding#isParameter()} when binding resolution is available.
* Falls back to checking if the AST node is a {@link SingleVariableDeclaration}
* inside a {@link MethodDeclaration}.</p>
*
* Args: [placeholderName]
* @since 1.4.2
*/
private static boolean evaluateIsParameter(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
IBinding binding = resolveBinding(node);
if (binding instanceof IVariableBinding vb) {
return vb.isParameter();
}
// Fallback: check AST structure
if (node instanceof SingleVariableDeclaration) {
return node.getParent() instanceof MethodDeclaration;
}
if (node instanceof SimpleName simpleName) {
ASTNode parent = simpleName.getParent();
if (parent instanceof SingleVariableDeclaration svd) {
return svd.getParent() instanceof MethodDeclaration;
}
}
return false;
}
/**
* Checks if a binding is a field (instance or static).
*
* <p>Uses {@link IVariableBinding#isField()} when binding resolution is available.
* Falls back to checking if the AST node is inside a {@link FieldDeclaration}.</p>
*
* Args: [placeholderName]
* @since 1.4.2
*/
private static boolean evaluateIsField(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
IBinding binding = resolveBinding(node);
if (binding instanceof IVariableBinding vb) {
return vb.isField();
}
// Fallback: check AST structure
if (node instanceof FieldDeclaration) {
return true;
}
if (node instanceof VariableDeclarationFragment vdf) {
return vdf.getParent() instanceof FieldDeclaration;
}
if (node instanceof SimpleName simpleName) {
ASTNode parent = simpleName.getParent();
if (parent instanceof VariableDeclarationFragment vdf) {
return vdf.getParent() instanceof FieldDeclaration;
}
}
return false;
}
/**
* Checks if the matched node is inside a constructor.
*
* <p>Walks up the AST from the matched node looking for a
* {@link MethodDeclaration} that {@linkplain MethodDeclaration#isConstructor() is a constructor}.</p>
*
* <p>This guard takes no arguments — it operates on the matched node context.</p>
*
* Args: (none)
* @since 1.4.2
*/
private static boolean evaluateIsInConstructor(GuardContext ctx, Object... args) {
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
ASTNode current = node;
while (current != null) {
if (current instanceof MethodDeclaration methodDecl) {
return methodDecl.isConstructor();
}
current = current.getParent();
}
return false;
}
/**
* Checks if the enclosing class overrides a given method name.
*
* <p>Scans the enclosing {@link TypeDeclaration} for a method with the given name.
* This is a structural check — it does not verify that the method actually overrides
* a superclass method (that would require full type hierarchy resolution).</p>
*
* Args: [methodName]
* @since 1.4.2
*/
private static boolean evaluateClassOverrides(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String methodName = stripQuotes(args[0].toString());
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
TypeDeclaration typeDecl = findEnclosingTypeDeclaration(node);
if (typeDecl == null) {
return false;
}
for (MethodDeclaration method : typeDecl.getMethods()) {
if (methodName.equals(method.getName().getIdentifier())) {
return true;
}
}
return false;
}
/**
* Checks if a binding is a local variable (not a field and not a parameter).
*
* <p>Uses {@link IVariableBinding#isField()} and {@link IVariableBinding#isParameter()}
* when binding resolution is available. Falls back to AST structure by checking
* whether the node is inside a {@link VariableDeclarationStatement}.</p>
*
* Args: [placeholderName]
*/
private static boolean evaluateIsLocalVariable(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
IBinding binding = resolveBinding(node);
if (binding instanceof IVariableBinding vb) {
return !vb.isField() && !vb.isParameter();
}
// Fallback: walk up AST and check for VariableDeclarationStatement
ASTNode current = node;
while (current != null) {
if (current instanceof VariableDeclarationStatement) {
return true;
}
if (current instanceof FieldDeclaration
|| current instanceof MethodDeclaration) {
return false;
}
current = current.getParent();
}
return false;
}
/**
* Checks if the matched expression is the initializer of a local variable declaration.
*
* <p>Returns {@code true} when the matched node appears as the initializer expression
* of a {@link VariableDeclarationFragment} whose parent is a
* {@link VariableDeclarationStatement} (not a {@link FieldDeclaration}).
* This guard takes no arguments — it operates on the matched node context.</p>
*
* <p>Use this guard when a rule applies only to local variable initializers, for
* example when suggesting {@code var} or when replacing a type that is thread-safe
* only as a local variable.</p>
*
* Args: (none)
*/
private static boolean evaluateIsAssignedToLocalVariable(GuardContext ctx, Object... args) {
ASTNode node = ctx.getMatchedNode();
if (node == null) {
return false;
}
// The matched node must be the initializer of a VariableDeclarationFragment
ASTNode parent = node.getParent();
if (!(parent instanceof VariableDeclarationFragment vdf)) {
return false;
}
// The initializer of the fragment must be our node
if (vdf.getInitializer() != node) {
return false;
}
// The fragment's parent must be a VariableDeclarationStatement (local), not FieldDeclaration
return vdf.getParent() instanceof VariableDeclarationStatement;
}
/**
* Checks if the type of the bound placeholder implements {@link java.lang.AutoCloseable}.
*
* <p>Uses the type binding of the bound expression when resolution is available.
* Walks the superinterface hierarchy to find {@code java.lang.AutoCloseable}.
* Falls back to {@code true} (conservative) when binding resolution is not available,
* so the rule still fires and lets the user decide.</p>
*
* Args: [placeholderName]
*/
private static boolean evaluateIsAutoCloseable(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
if (!(node instanceof Expression expr)) {
return false;
}
ITypeBinding typeBinding = expr.resolveTypeBinding();
if (typeBinding == null) {
// Cannot resolve — allow the rule to fire conservatively
return true;
}
return implementsAutoCloseable(typeBinding);
}
/**
* Returns {@code true} if the given type binding implements {@code java.lang.AutoCloseable}.
*/
private static boolean implementsAutoCloseable(ITypeBinding typeBinding) {
if (typeBinding == null) {
return false;
}
String qualifiedName = typeBinding.getQualifiedName();
if ("java.lang.AutoCloseable".equals(qualifiedName) //$NON-NLS-1$
|| "java.io.Closeable".equals(qualifiedName)) { //$NON-NLS-1$
return true;
}
for (ITypeBinding iface : typeBinding.getInterfaces()) {
if (implementsAutoCloseable(iface)) {
return true;
}
}
ITypeBinding superClass = typeBinding.getSuperclass();
return implementsAutoCloseable(superClass);
}
/**
* Checks if the generic type parameter at a given index of a bound placeholder's type
* matches the expected type name.
*
* <p>Example usage in DSL guard:
* {@code genericTypeIs($var, 0, "java.lang.String")} — checks that the first
* type parameter of {@code $var}'s type is {@code String}.</p>
*
* <p><b>Graceful degradation:</b> Returns {@code true} when binding resolution
* is not available, so rules still fire conservatively.</p>
*
* Args: [placeholderName, paramIndex, typeName]
*
* @since 1.4.2
*/
private static boolean evaluateGenericTypeIs(GuardContext ctx, Object... args) {
if (args.length < 3) {
return false;
}
String placeholderName = args[0].toString();
int paramIndex;
try {
paramIndex = Integer.parseInt(args[1].toString());
} catch (NumberFormatException e) {
return false;
}
String expectedTypeName = args[2].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
ITypeBinding typeBinding = resolveTypeBinding(node);
if (typeBinding == null) {
// Graceful degradation: no binding info → accept conservatively
return true;
}
ITypeBinding[] typeArgs = typeBinding.getTypeArguments();
if (typeArgs == null || paramIndex < 0 || paramIndex >= typeArgs.length) {
return false;
}
String actualName = typeArgs[paramIndex].getQualifiedName();
return expectedTypeName.equals(actualName)
|| expectedTypeName.equals(typeArgs[paramIndex].getName());
}
/**
* Checks the number of matched variadic arguments for a multi-placeholder.
*
* <p>Example usage in DSL guard:
* {@code argsCount($args$, 3)} — checks that exactly 3 arguments were captured.</p>
*
* Args: [placeholderName, expectedCount]
*
* @since 1.4.2
*/
private static boolean evaluateArgsCount(GuardContext ctx, Object... args) {
if (args.length < 2) {
return false;
}
String placeholderName = args[0].toString();
int expectedCount;
try {
expectedCount = Integer.parseInt(args[1].toString());
} catch (NumberFormatException e) {
return false;
}
Object binding = ctx.getMatch().getBindings().get(placeholderName);
if (binding instanceof List<?> list) {
return list.size() == expectedCount;
}
// Single binding counts as 1
if (binding != null) {
return expectedCount == 1;
}
return expectedCount == 0;
}
/**
* Checks if the matched placeholder node is a variable declaration whose type
* implements {@code AutoCloseable} and is <b>not</b> already inside a
* try-with-resources block.
*
* <p>This guard is useful for identifying resource variables that could
* benefit from being wrapped in try-with-resources statements.</p>
*
* <p>Example usage in DSL guard:
* {@code isResourceVariable($var)} — returns true if {@code $var} is a
* local variable of an AutoCloseable type not already managed by try-with-resources.</p>
*
* Args: [placeholderName]
*
* @since 1.4.2
*/
private static boolean evaluateIsResourceVariable(GuardContext ctx, Object... args) {
if (args.length < 1) {
return false;
}
String placeholderName = args[0].toString();
ASTNode node = ctx.getBinding(placeholderName);
if (node == null) {
return false;
}
// Check if the node's type implements AutoCloseable
ITypeBinding typeBinding = resolveTypeBinding(node);
if (typeBinding == null) {
// Graceful degradation
return true;
}
if (!implementsAutoCloseable(typeBinding)) {
return false;
}
// Check if already in a try-with-resources block
ASTNode parent = node.getParent();
while (parent != null) {
if (parent instanceof TryStatement tryStmt) {
@SuppressWarnings("unchecked")
List<Expression> resources = tryStmt.resources();
if (!resources.isEmpty()) {
// This is a try-with-resources, check if our node is within a resource
for (Expression resource : resources) {
if (isAncestorOrSelf(resource, node)) {
return false; // Already managed by try-with-resources
}
}
}
}
parent = parent.getParent();
}
return true;
}
}