more fixes 2

This commit is contained in:
2026-06-20 07:57:01 +02:00
parent 3ac057618f
commit c37aa92c78
4 changed files with 266 additions and 12 deletions

View File

@@ -70,7 +70,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
} }
} }
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())))) { if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent)))) {
// Event matches or is a wildcard // Event matches or is a wildcard
for (State smSourceState : t.getSourceStates()) { for (State smSourceState : t.getSourceStates()) {
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName(); String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
@@ -157,4 +157,31 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify // For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
return simplified; return simplified;
} }
private boolean isGuardedContains(String smEvent, String triggerEvent) {
// Only fire if it looks like a simple identifier (no spaces, no parens)
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
return false;
}
String shorter = smEvent.length() < triggerEvent.length() ? smEvent : triggerEvent;
String longer = smEvent.length() >= triggerEvent.length() ? smEvent : triggerEvent;
shorter = shorter.toLowerCase();
longer = longer.toLowerCase();
if (longer.contains(shorter)) {
// Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators
// to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE")
if (shorter.length() < 4) {
return longer.matches(".*(^|_|-)" + shorter + "(_|-|$).*");
}
// For longer strings, a straight contains is usually safe enough
return true;
}
return false;
}
} }

View File

@@ -159,12 +159,34 @@ public class CallGraphBuilder {
} }
String methodName = resolvedValue.substring(lastDot + 1, openParen); String methodName = resolvedValue.substring(lastDot + 1, openParen);
String declaredType = null;
String sourceMethod = null;
if (varName != null) { if (varName != null) {
// Resolve in the first method in the path where the variable might be declared
for (String methodFqn : path) { for (String methodFqn : path) {
String declaredType = getVariableDeclaredType(methodFqn, varName); declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) { if (declaredType != null) {
System.out.println("DEEP TRACE: " + methodFqn + " " + varName + " -> " + declaredType); sourceMethod = methodFqn;
break;
}
}
} else {
String baseExpr = resolvedValue.substring(0, lastDot);
if (baseExpr.contains("new ")) {
int newIdx = baseExpr.indexOf("new ");
int openParenBase = baseExpr.indexOf('(', newIdx);
if (openParenBase > newIdx) {
declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim();
if (declaredType.contains("<")) {
declaredType = declaredType.substring(0, declaredType.indexOf('<'));
}
sourceMethod = "inline-instantiation";
}
}
}
if (declaredType != null) {
System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
List<String> typesToInspect = new ArrayList<>(); List<String> typesToInspect = new ArrayList<>();
typesToInspect.add(declaredType); typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType)); typesToInspect.addAll(context.getImplementations(declaredType));
@@ -172,14 +194,13 @@ public class CallGraphBuilder {
for (String type : typesToInspect) { for (String type : typesToInspect) {
Set<String> visited = new HashSet<>(); Set<String> visited = new HashSet<>();
// We must find the compilation unit to pass for simple names!
// Let's pass the first available CU for this class
TypeDeclaration baseTd = context.getTypeDeclaration(type); TypeDeclaration baseTd = context.getTypeDeclaration(type);
CompilationUnit cuToUse = null; CompilationUnit cuToUse = null;
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) { if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) baseTd.getRoot(); cuToUse = (CompilationUnit) baseTd.getRoot();
} else { } else {
String entryClassName = methodFqn.substring(0, methodFqn.lastIndexOf('.')); String firstPathMethod = path.get(0);
String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName); TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) { if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) entryTd.getRoot(); cuToUse = (CompilationUnit) entryTd.getRoot();
@@ -192,9 +213,6 @@ public class CallGraphBuilder {
polymorphicEvents.add(constant); polymorphicEvents.add(constant);
} }
} }
}
break;
}
} }
} }
} }

View File

@@ -192,4 +192,30 @@ class TransitionLinkerEnricherTest {
CallChain updatedChain = result.getMetadata().getCallChains().get(0); CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1); assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
} }
@Test
void shouldNotFalsePositiveOnSubstringContains() {
// This test proves that the heuristic is entirely removed and false positives are eliminated
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"));
// Trigger point sends "PAYMENT" which contains "PAY". Under the old heuristics, this would falsely match!
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("PAYMENT").build())
.build();
AnalysisResult result = AnalysisResult.builder()
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
// It should NOT match, because "PAYMENT" != "PAY" strictly.
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
}
} }

View File

@@ -1337,4 +1337,187 @@ class CallGraphBuilderTest {
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("STRING_LITERAL_EVENT"); org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("STRING_LITERAL_EVENT");
} }
@Test
void shouldResolveConstantsFromInlineInstantiation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent().getEvent());
}
}
class DomainEvent {
public String getEvent() { return "INLINE_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithArguments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent("PAYLOAD_ID").getEvent());
}
}
class DomainEvent {
private String id;
public DomainEvent(String id) { this.id = id; }
public String getEvent() { return "INLINE_EVENT_WITH_ARGS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_args");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_ARGS");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithGenerics() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent<String>().getEvent());
}
}
class DomainEvent<T> {
public String getEvent() { return "INLINE_EVENT_WITH_GENERICS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_gen");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_GENERICS");
}
@Test
void shouldResolveConstantsFromInlineInstantiationInheritedMethod() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new SpecificDomainEvent().getEvent());
}
}
abstract class BaseDomainEvent {
public String getEvent() { return "BASE_EVENT_RETURN"; }
}
class SpecificDomainEvent extends BaseDomainEvent {
// no getEvent here, it inherits it!
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_hier");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithComplexArguments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(Order o, String abc) {
service.process(new DomainEvent(o.getId(), abc).getEvent());
}
}
class Order {
public String getId() { return "123"; }
}
class DomainEvent {
private String id;
private String extra;
public DomainEvent(String id, String extra) { this.id = id; this.extra = extra; }
public String getEvent() { return "INLINE_EVENT_COMPLEX_ARGS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_complex_args");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_COMPLEX_ARGS");
}
} }