Split boolean ternary endpoints into provable branches and scope payment exports.
Boolean request-param ternaries now expand into true/false binding variants so each call chain resolves a single event instead of AMBIGUOUS_WIDEN. Setter/constructor @Qualifier injection is traced for stateMachineId, and PaymentStateMachineConfig gets a dedicated golden with payment endpoints linked via paymentStateMachine. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -102,6 +102,9 @@ public final class BooleanConstraintEvaluator {
|
||||
if (boundValue.startsWith("\"") && boundValue.endsWith("\"")) {
|
||||
return true;
|
||||
}
|
||||
if ("true".equalsIgnoreCase(boundValue) || "false".equalsIgnoreCase(boundValue)) {
|
||||
return true;
|
||||
}
|
||||
if (boundValue.contains(".") && Character.isUpperCase(boundValue.charAt(boundValue.lastIndexOf('.') + 1))) {
|
||||
return true;
|
||||
}
|
||||
@@ -174,6 +177,10 @@ public final class BooleanConstraintEvaluator {
|
||||
matcher.appendReplacement(sb, matches ? "true" : "false");
|
||||
}
|
||||
matcher.appendTail(sb);
|
||||
if ("true".equalsIgnoreCase(cleanValue) || "false".equalsIgnoreCase(cleanValue)) {
|
||||
Pattern bareVar = Pattern.compile("(?<![\\w.])" + Pattern.quote(varName) + "(?![\\w])");
|
||||
return bareVar.matcher(expr).replaceAll(cleanValue.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
startMethods.add(startMethod);
|
||||
|
||||
List<Map<String, String>> bindingVariants =
|
||||
EntryPointBindingExpander.expandPathVariableBindings(ep, context, callGraph);
|
||||
EntryPointBindingExpander.expandEntryPointBindings(ep, context, callGraph);
|
||||
if (bindingVariants.isEmpty()) {
|
||||
bindingVariants = List.of(Map.of());
|
||||
}
|
||||
@@ -213,6 +213,36 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return triggerConstraint;
|
||||
}
|
||||
|
||||
private static Expression selectTernaryBranch(ConditionalExpression cond, Map<String, String> bindings) {
|
||||
if (cond == null || bindings == null || bindings.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Expression condition = cond.getExpression();
|
||||
if (condition instanceof PrefixExpression prefixExpression
|
||||
&& prefixExpression.getOperator() == PrefixExpression.Operator.NOT
|
||||
&& prefixExpression.getOperand() instanceof SimpleName negatedName) {
|
||||
String bound = bindings.get(negatedName.getIdentifier());
|
||||
if ("true".equals(bound)) {
|
||||
return cond.getElseExpression();
|
||||
}
|
||||
if ("false".equals(bound)) {
|
||||
return cond.getThenExpression();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!(condition instanceof SimpleName simpleName)) {
|
||||
return null;
|
||||
}
|
||||
String bound = bindings.get(simpleName.getIdentifier());
|
||||
if ("true".equals(bound)) {
|
||||
return cond.getThenExpression();
|
||||
}
|
||||
if ("false".equals(bound)) {
|
||||
return cond.getElseExpression();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected TriggerPoint resolveTriggerPointParametersOriginal(
|
||||
TriggerPoint tp,
|
||||
List<String> path,
|
||||
@@ -679,9 +709,16 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
polymorphicEvents.add(declaredType);
|
||||
}
|
||||
} else if (exprNode instanceof ConditionalExpression cond) {
|
||||
Expression selectedBranch = selectTernaryBranch(cond, initialBindings);
|
||||
List<Expression> branches = new ArrayList<>();
|
||||
branches.add(cond.getThenExpression());
|
||||
branches.add(cond.getElseExpression());
|
||||
if (selectedBranch != null) {
|
||||
branches.add(selectedBranch);
|
||||
resolvedValue = selectedBranch.toString();
|
||||
exprNode = selectedBranch;
|
||||
} else {
|
||||
branches.add(cond.getThenExpression());
|
||||
branches.add(cond.getElseExpression());
|
||||
}
|
||||
for (Expression branch : branches) {
|
||||
if (branch instanceof ConditionalExpression nestedCond) {
|
||||
// traceVariableAll collapses literal "true"/"false" conditions to a single branch;
|
||||
@@ -2192,6 +2229,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (value == null || value.isBlank() || value.contains("(")) {
|
||||
continue;
|
||||
}
|
||||
if ("true".equals(value) || "false".equals(value)) {
|
||||
continue;
|
||||
}
|
||||
parts.add("\"" + value + "\".equalsIgnoreCase(" + entry.getKey() + ")");
|
||||
}
|
||||
return parts.isEmpty() ? null : String.join(" && ", parts);
|
||||
|
||||
@@ -16,6 +16,33 @@ public final class EntryPointBindingExpander {
|
||||
private EntryPointBindingExpander() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands an entry point into concrete binding variants from path/query parameters:
|
||||
* string switch/if arms and boolean ternary conditions provable in source.
|
||||
*/
|
||||
public static List<Map<String, String>> expandEntryPointBindings(
|
||||
EntryPoint entryPoint,
|
||||
CodebaseContext context,
|
||||
Map<String, List<CallEdge>> callGraph) {
|
||||
List<Map<String, String>> pathVariants = expandPathVariableBindings(entryPoint, context, callGraph);
|
||||
List<Map<String, String>> booleanVariants = expandBooleanTernaryBindings(entryPoint, context, callGraph);
|
||||
if (pathVariants.isEmpty()) {
|
||||
return booleanVariants;
|
||||
}
|
||||
if (booleanVariants.isEmpty()) {
|
||||
return pathVariants;
|
||||
}
|
||||
List<Map<String, String>> combined = new ArrayList<>();
|
||||
for (Map<String, String> pathVariant : pathVariants) {
|
||||
for (Map<String, String> booleanVariant : booleanVariants) {
|
||||
Map<String, String> merged = new LinkedHashMap<>(pathVariant);
|
||||
merged.putAll(booleanVariant);
|
||||
combined.add(merged);
|
||||
}
|
||||
}
|
||||
return combined;
|
||||
}
|
||||
|
||||
public static List<Map<String, String>> expandPathVariableBindings(
|
||||
EntryPoint entryPoint,
|
||||
CodebaseContext context,
|
||||
@@ -55,14 +82,37 @@ public final class EntryPointBindingExpander {
|
||||
Map<String, String> metadata = entryPoint.getMetadata() == null
|
||||
? new LinkedHashMap<>()
|
||||
: new LinkedHashMap<>(entryPoint.getMetadata());
|
||||
StringBuilder querySuffix = new StringBuilder();
|
||||
for (Map.Entry<String, String> binding : bindings.entrySet()) {
|
||||
String placeholder = "{" + binding.getKey() + "}";
|
||||
boolean pathPlaceholder = name != null && name.contains(placeholder);
|
||||
String path = metadata.get("path");
|
||||
if (!pathPlaceholder && path != null) {
|
||||
pathPlaceholder = path.contains(placeholder);
|
||||
}
|
||||
if (pathPlaceholder) {
|
||||
if (name != null) {
|
||||
name = name.replace(placeholder, binding.getValue());
|
||||
}
|
||||
if (path != null) {
|
||||
metadata.put("path", path.replace(placeholder, binding.getValue()));
|
||||
}
|
||||
} else if (isBooleanBindingValue(binding.getValue())) {
|
||||
if (querySuffix.isEmpty()) {
|
||||
querySuffix.append('?');
|
||||
} else {
|
||||
querySuffix.append('&');
|
||||
}
|
||||
querySuffix.append(binding.getKey()).append('=').append(binding.getValue());
|
||||
}
|
||||
}
|
||||
if (!querySuffix.isEmpty()) {
|
||||
if (name != null) {
|
||||
name = name.replace(placeholder, binding.getValue());
|
||||
name = name + querySuffix;
|
||||
}
|
||||
String path = metadata.get("path");
|
||||
if (path != null) {
|
||||
metadata.put("path", path.replace(placeholder, binding.getValue()));
|
||||
metadata.put("path", path + querySuffix);
|
||||
}
|
||||
}
|
||||
return EntryPoint.builder()
|
||||
@@ -92,6 +142,116 @@ public final class EntryPointBindingExpander {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<Map<String, String>> expandBooleanTernaryBindings(
|
||||
EntryPoint entryPoint,
|
||||
CodebaseContext context,
|
||||
Map<String, List<CallEdge>> callGraph) {
|
||||
if (entryPoint == null || entryPoint.getParameters() == null || entryPoint.getParameters().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||
if (!isBooleanType(parameter.getType())) {
|
||||
continue;
|
||||
}
|
||||
if (hasBooleanTernaryForParameter(
|
||||
entryPoint.getClassName() + "." + entryPoint.getMethodName(),
|
||||
parameter.getName(),
|
||||
context,
|
||||
callGraph)) {
|
||||
return List.of(
|
||||
Map.of(parameter.getName(), "true"),
|
||||
Map.of(parameter.getName(), "false"));
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private static boolean isBooleanType(String type) {
|
||||
return "boolean".equals(type) || "Boolean".equals(type);
|
||||
}
|
||||
|
||||
private static boolean isBooleanBindingValue(String value) {
|
||||
return "true".equals(value) || "false".equals(value);
|
||||
}
|
||||
|
||||
private static boolean hasBooleanTernaryForParameter(
|
||||
String startMethod,
|
||||
String paramName,
|
||||
CodebaseContext context,
|
||||
Map<String, List<CallEdge>> callGraph) {
|
||||
ArrayDeque<String> queue = new ArrayDeque<>();
|
||||
Set<String> visited = new HashSet<>();
|
||||
queue.add(startMethod);
|
||||
while (!queue.isEmpty()) {
|
||||
String methodFqn = queue.poll();
|
||||
if (!visited.add(methodFqn)) {
|
||||
continue;
|
||||
}
|
||||
if (visited.size() > 12) {
|
||||
break;
|
||||
}
|
||||
if (containsBooleanTernaryCondition(methodFqn, paramName, context)) {
|
||||
return true;
|
||||
}
|
||||
List<CallEdge> edges = callGraph.get(methodFqn);
|
||||
if (edges == null) {
|
||||
continue;
|
||||
}
|
||||
for (CallEdge edge : edges) {
|
||||
if (edge.getTargetMethod() != null) {
|
||||
queue.add(edge.getTargetMethod());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean containsBooleanTernaryCondition(String methodFqn, String paramName, CodebaseContext context) {
|
||||
MethodDeclaration method = findMethodDeclaration(methodFqn, context);
|
||||
if (method == null || method.getBody() == null) {
|
||||
return false;
|
||||
}
|
||||
if (!hasParameter(method, paramName)) {
|
||||
return false;
|
||||
}
|
||||
final boolean[] found = {false};
|
||||
method.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ConditionalExpression node) {
|
||||
if (isBooleanConditionForParameter(node.getExpression(), paramName)) {
|
||||
found[0] = true;
|
||||
return false;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
for (Object argObj : node.arguments()) {
|
||||
if (argObj instanceof ConditionalExpression cond
|
||||
&& isBooleanConditionForParameter(cond.getExpression(), paramName)) {
|
||||
found[0] = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return found[0];
|
||||
}
|
||||
|
||||
private static boolean isBooleanConditionForParameter(Expression condition, String paramName) {
|
||||
if (condition instanceof SimpleName simpleName) {
|
||||
return paramName.equals(simpleName.getIdentifier());
|
||||
}
|
||||
if (condition instanceof PrefixExpression prefixExpression
|
||||
&& prefixExpression.getOperator() == PrefixExpression.Operator.NOT
|
||||
&& prefixExpression.getOperand() instanceof SimpleName simpleName) {
|
||||
return paramName.equals(simpleName.getIdentifier());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
|
||||
if (parameter.getAnnotations() == null) {
|
||||
return false;
|
||||
|
||||
@@ -471,9 +471,48 @@ public class GenericEventDetector {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (enclosing instanceof TypeDeclaration typeDeclaration) {
|
||||
for (MethodDeclaration method : typeDeclaration.getMethods()) {
|
||||
if (!hasAutowiredAnnotation(method) || method.parameters().size() != 1) {
|
||||
continue;
|
||||
}
|
||||
SingleVariableDeclaration param = (SingleVariableDeclaration) method.parameters().get(0);
|
||||
if (!matchesInjectionTarget(method, fieldName, param)) {
|
||||
continue;
|
||||
}
|
||||
String qualifier = extractQualifierAnnotation(param.modifiers());
|
||||
if (qualifier != null) {
|
||||
return qualifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean hasAutowiredAnnotation(MethodDeclaration method) {
|
||||
for (Object modifierObj : method.modifiers()) {
|
||||
if (modifierObj instanceof Annotation annotation
|
||||
&& annotation.getTypeName().toString().endsWith("Autowired")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean matchesInjectionTarget(
|
||||
MethodDeclaration method, String fieldName, SingleVariableDeclaration param) {
|
||||
if (fieldName.equals(param.getName().getIdentifier())) {
|
||||
return true;
|
||||
}
|
||||
String methodName = method.getName().getIdentifier();
|
||||
if (methodName.startsWith("set") && methodName.length() > 3) {
|
||||
String propertyName = methodName.substring(3);
|
||||
return propertyName.equalsIgnoreCase(fieldName)
|
||||
|| (Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1)).equals(fieldName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String extractQualifierFromBinding(IVariableBinding binding) {
|
||||
if (binding == null) {
|
||||
return null;
|
||||
@@ -484,16 +523,31 @@ public class GenericEventDetector {
|
||||
}
|
||||
if (binding.isField() && binding.getDeclaringClass() != null) {
|
||||
for (IMethodBinding method : binding.getDeclaringClass().getDeclaredMethods()) {
|
||||
if (!method.isConstructor()) {
|
||||
boolean isAutowiredMethod = false;
|
||||
for (IAnnotationBinding ann : method.getAnnotations()) {
|
||||
if (ann.getAnnotationType() != null
|
||||
&& "org.springframework.beans.factory.annotation.Autowired"
|
||||
.equals(ann.getAnnotationType().getQualifiedName())) {
|
||||
isAutowiredMethod = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!method.isConstructor() && !isAutowiredMethod) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < method.getParameterTypes().length; i++) {
|
||||
if (method.getParameterTypes()[i].isEqualTo(binding.getType().getErasure())
|
||||
&& binding.getName().equals(method.getParameterNames()[i])) {
|
||||
String paramQualifier = readQualifierFromAnnotations(method.getParameterAnnotations(i));
|
||||
if (paramQualifier != null) {
|
||||
return paramQualifier;
|
||||
}
|
||||
if (!method.getParameterTypes()[i].isEqualTo(binding.getType().getErasure())) {
|
||||
continue;
|
||||
}
|
||||
if (method.isConstructor() && !binding.getName().equals(method.getParameterNames()[i])) {
|
||||
continue;
|
||||
}
|
||||
if (!method.isConstructor() && !matchesSetterParameterName(method.getName(), binding.getName())) {
|
||||
continue;
|
||||
}
|
||||
String paramQualifier = readQualifierFromAnnotations(method.getParameterAnnotations(i));
|
||||
if (paramQualifier != null) {
|
||||
return paramQualifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,6 +555,18 @@ public class GenericEventDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean matchesSetterParameterName(String methodName, String fieldName) {
|
||||
if (methodName == null || fieldName == null) {
|
||||
return false;
|
||||
}
|
||||
if (methodName.startsWith("set") && methodName.length() > 3) {
|
||||
String propertyName = methodName.substring(3);
|
||||
return propertyName.equalsIgnoreCase(fieldName)
|
||||
|| (Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1)).equals(fieldName);
|
||||
}
|
||||
return fieldName.equals(methodName);
|
||||
}
|
||||
|
||||
private String readQualifierFromAnnotations(IAnnotationBinding[] annotations) {
|
||||
if (annotations == null) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user