diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java index b7f7e81..1897f22 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java @@ -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 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) { diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java index be97046..2193fa5 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java @@ -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(); + } } \ No newline at end of file