Perf: use CFG dataflow for traceLocalSetter and guard hot-path logging.

Replace per-call method-body AST scans with reaching-definitions lookup, guard remaining debug/trace calls in call-graph hot paths, and add branch edge-case tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 10:14:02 +02:00
parent 82a70b5374
commit 411b7a0e52
7 changed files with 571 additions and 81 deletions

View File

@@ -0,0 +1,228 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import org.eclipse.jdt.core.dom.Expression;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class FindLocalSetterArgumentTest {
private CodebaseContext context;
private VariableTracer variableTracer;
@BeforeEach
void setUp(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Payload.java", """
package com.example;
public class Payload {
private String event = "DEFAULT";
public String getEvent() { return event; }
public void setEvent(String event) { this.event = event; }
}
""");
context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
variableTracer = new VariableTracer(context, context.getConstantResolver());
}
@Test
void lastSetterBeforeGetterWins(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Payload payload = new Payload();
payload.setEvent("FIRST");
payload.setEvent("SECOND");
String value = payload.getEvent();
}
}
""");
context.scan(tempDir);
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNotNull();
assertThat(result.toString()).contains("SECOND");
}
@Test
void setterAfterGetterIsIgnored(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Payload payload = new Payload();
payload.setEvent("BEFORE");
String value = payload.getEvent();
payload.setEvent("AFTER");
}
}
""");
context.scan(tempDir);
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNotNull();
assertThat(result.toString()).contains("BEFORE");
assertThat(result.toString()).doesNotContain("AFTER");
}
@Test
void ifElseBranchesBothReachGetter_lastOnCfgPathWins(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run(boolean flag) {
Payload payload = new Payload();
if (flag) {
payload.setEvent("IF_BRANCH");
} else {
payload.setEvent("ELSE_BRANCH");
}
String value = payload.getEvent();
}
}
""");
context.scan(tempDir);
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNotNull();
// Both branches are on the CFG path to the getter; last in source order wins.
assertThat(result.toString()).contains("ELSE_BRANCH");
}
@Test
void setterOnSiblingBranchIsExcluded(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run(boolean flag) {
Payload payload = new Payload();
if (flag) {
payload.setEvent("TAKEN");
String value = payload.getEvent();
return;
}
payload.setEvent("OTHER_BRANCH");
}
}
""");
context.scan(tempDir);
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNotNull();
assertThat(result.toString()).contains("TAKEN");
assertThat(result.toString()).doesNotContain("OTHER_BRANCH");
}
@Test
void noGetterCallFallsBackToOrderedScan(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Payload payload = new Payload();
payload.setEvent("ONLY");
}
}
""");
context.scan(tempDir);
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNotNull();
assertThat(result.toString()).contains("ONLY");
}
@Test
void fieldAssignmentSetterIsFound(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/MutablePayload.java", """
package com.example;
public class MutablePayload {
public String event;
public String getEvent() { return event; }
}
""");
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
MutablePayload payload = new MutablePayload();
payload.event = "ASSIGNED";
String value = payload.getEvent();
}
}
""");
context.scan(tempDir);
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNotNull();
assertThat(result.toString()).contains("ASSIGNED");
}
@Test
void setterInsideNestedExpressionIsFound(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Payload payload = new Payload();
consume(payload.setEvent("NESTED"));
String value = payload.getEvent();
}
private void consume(String ignored) {}
}
""");
context.scan(tempDir);
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNotNull();
assertThat(result.toString()).contains("NESTED");
}
@Test
void returnsNullWhenNoMatchingSetter(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Payload payload = new Payload();
String value = payload.getEvent();
}
}
""");
context.scan(tempDir);
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNull();
}
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
Path file = tempDir.resolve(relativePath);
Files.createDirectories(file.getParent());
Files.writeString(file, source);
}
}