This commit is contained in:
2026-07-04 07:25:11 +02:00
parent 4d7cda56f5
commit 0165da5f51
2 changed files with 140 additions and 14 deletions

View File

@@ -120,25 +120,72 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
cleanMachine = cleanMachine.substring(0, cleanMachine.length() - "CONFIG".length());
}
String constraintUpper = constraint.toUpperCase();
String expr = constraint;
java.util.List<String> allMachines = java.util.List.of("ORDER", "DOCUMENT", "USER");
for (String m : allMachines) {
if (!m.equals(cleanMachine)) {
if (constraintUpper.contains(m)) {
int idx = constraintUpper.indexOf(m);
String sub = constraintUpper.substring(0, idx);
String cleanedSub = sub.replaceAll("[\\s\"(]+$", "");
boolean isNegated = cleanedSub.endsWith("!") || cleanedSub.endsWith("NOT") || constraintUpper.contains("!=" + m) || constraintUpper.contains("! = " + m);
if (!isNegated) {
if (constraintUpper.contains("\"" + m + "\"") || constraintUpper.contains("'" + m + "'") || constraintUpper.contains(m + ".")) {
return false;
}
}
boolean isCurrent = m.equals(cleanMachine);
String replacement = isCurrent ? "true" : "false";
String regex1 = "(?i)[\"']?" + m + "[\"']?\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\([^)]*\\)";
String regex2 = "(?i)[a-zA-Z0-9._]+\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\(\\s*[\"']?" + m + "[\"']?\\s*\\)";
String regex3 = "(?i)[a-zA-Z0-9._]+\\s*==\\s*[\"']?" + m + "[\"']?";
String regex4 = "(?i)[a-zA-Z0-9._]+\\s*!=\\s*[\"']?" + m + "[\"']?";
expr = expr.replaceAll(regex1, replacement);
expr = expr.replaceAll(regex2, replacement);
expr = expr.replaceAll(regex3, replacement);
expr = expr.replaceAll(regex4, isCurrent ? "false" : "true");
}
try {
return BooleanEvaluator.evaluate(expr);
} catch (Exception e) {
return !expr.contains("false");
}
}
private static class BooleanEvaluator {
public static boolean evaluate(String expr) {
expr = expr.replaceAll("\\s+", "");
return parseExpr(expr);
}
private static boolean parseExpr(String s) {
if (s.isEmpty()) return true;
int parenDepth = 0;
for (int i = s.length() - 1; i >= 0; i--) {
char c = s.charAt(i);
if (c == ')') parenDepth++;
else if (c == '(') parenDepth--;
else if (parenDepth == 0 && i > 0 && s.charAt(i) == '|' && s.charAt(i - 1) == '|') {
return parseExpr(s.substring(0, i - 1)) || parseExpr(s.substring(i + 1));
}
}
parenDepth = 0;
for (int i = s.length() - 1; i >= 0; i--) {
char c = s.charAt(i);
if (c == ')') parenDepth++;
else if (c == '(') parenDepth--;
else if (parenDepth == 0 && i > 0 && s.charAt(i) == '&' && s.charAt(i - 1) == '&') {
return parseExpr(s.substring(0, i - 1)) && parseExpr(s.substring(i + 1));
}
}
if (s.startsWith("!")) {
return !parseExpr(s.substring(1));
}
if (s.startsWith("(") && s.endsWith(")")) {
return parseExpr(s.substring(1, s.length() - 1));
}
if (s.equalsIgnoreCase("true")) return true;
if (s.equalsIgnoreCase("false")) return false;
return true;
}
return true;
}
private String simplify(String name) {

View File

@@ -408,4 +408,83 @@ class TransitionLinkerEnricherTest {
assertThat(resComp.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
}
@Test
void shouldHandleMultipleNegations() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
// Case 1: Double Negation on ORDER -> !(!(ORDER)) -> Should act as POSITIVE (meaning order is required).
// Since we are running OrderStateMachineConfiguration, it should match!
CallChain chain1 = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("PAY")
.constraint("!(!(ORDER.equals(type)))")
.build())
.build();
AnalysisResult resultOrder = AnalysisResult.builder()
.name("com.example.OrderStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain1)).build())
.build();
enricher.enrich(resultOrder, null, null);
assertThat(resultOrder.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
// Case 2: Triple Negation on ORDER -> !(!(!(ORDER))) -> Should act as NEGATIVE (meaning order is rejected/forbidden).
// Under OrderStateMachineConfiguration, this chain should be rejected!
CallChain chain2 = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("PAY")
.constraint("!(!(!(ORDER.equals(type))))")
.build())
.build();
AnalysisResult resultOrder2 = AnalysisResult.builder()
.name("com.example.OrderStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain2)).build())
.build();
enricher.enrich(resultOrder2, null, null);
assertThat(resultOrder2.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
}
@Test
void shouldHandleComplexBooleanLogic() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
// Case 1: OR expression -> "ORDER".equals(type) || "DOCUMENT".equals(type)
// Under OrderStateMachineConfiguration, it should match!
CallChain chain1 = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("PAY")
.constraint("\"ORDER\".equalsIgnoreCase(type) || \"DOCUMENT\".equalsIgnoreCase(type)")
.build())
.build();
AnalysisResult resultOrder = AnalysisResult.builder()
.name("com.example.OrderStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain1)).build())
.build();
enricher.enrich(resultOrder, null, null);
assertThat(resultOrder.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
// Case 2: Under UserStateMachineConfiguration, it should be rejected!
AnalysisResult resultUser = AnalysisResult.builder()
.name("com.example.UserStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain1)).build())
.build();
enricher.enrich(resultUser, null, null);
assertThat(resultUser.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
}
}