This commit is contained in:
2026-06-27 19:18:32 +02:00
parent 5c7cf3639b
commit 76bb88e065
16 changed files with 1199 additions and 100 deletions

View File

@@ -9,6 +9,7 @@ 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;
@@ -88,4 +89,42 @@ public class ResolutionRobustnessTest {
assertThat(context.getTypeDeclaration("p1.X")).isNotNull();
assertThat(context.getTypeDeclaration("p2.X")).isNotNull();
}
@Test
void testJdtBindingsResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Base.java"),
"package com.foo;\n" +
"public class Base {\n" +
" public void run() {}\n" +
"}"
);
Files.writeString(comFoo.resolve("Child.java"),
"package com.foo;\n" +
"public class Child extends Base {\n" +
" public void execute() {\n" +
" run();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
assertThat(childTd).isNotNull();
// Resolve the binding for Child class
org.eclipse.jdt.core.dom.ITypeBinding binding = childTd.resolveBinding();
assertThat(binding).isNotNull();
assertThat(binding.getQualifiedName()).isEqualTo("com.foo.Child");
// Verify we can get the superclass binding
org.eclipse.jdt.core.dom.ITypeBinding superBinding = binding.getSuperclass();
assertThat(superBinding).isNotNull();
assertThat(superBinding.getQualifiedName()).isEqualTo("com.foo.Base");
}
}

View File

@@ -0,0 +1,92 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
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 BindingResolverTest {
@Test
void testBindingResolverMethods(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Base.java"),
"package com.foo;\n" +
"public interface Base {\n" +
" void run();\n" +
"}"
);
Files.writeString(comFoo.resolve("Child.java"),
"package com.foo;\n" +
"public class Child implements Base {\n" +
" public void run() {}\n" +
" public void execute() {\n" +
" Child c = new Child();\n" +
" c.run();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
assertThat(childTd).isNotNull();
ITypeBinding binding = BindingResolver.resolveType(childTd);
assertThat(binding).isNotNull();
assertThat(BindingResolver.getFullyQualifiedName(binding)).isEqualTo("com.foo.Child");
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.Base")).isTrue();
assertThat(BindingResolver.isSubtypeOf(binding, "java.lang.Object")).isTrue();
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.NonExistent")).isFalse();
// Find MethodDeclaration execute
MethodDeclaration executeMethod = null;
for (MethodDeclaration md : childTd.getMethods()) {
if (md.getName().getIdentifier().equals("execute")) {
executeMethod = md;
}
}
assertThat(executeMethod).isNotNull();
// Traverse the body to find ClassInstanceCreation and MethodInvocation
class Visitor extends ASTVisitor {
ClassInstanceCreation cic = null;
MethodInvocation mi = null;
@Override
public boolean visit(ClassInstanceCreation node) {
cic = node;
return super.visit(node);
}
@Override
public boolean visit(MethodInvocation node) {
mi = node;
return super.visit(node);
}
}
Visitor visitor = new Visitor();
executeMethod.accept(visitor);
assertThat(visitor.cic).isNotNull();
IMethodBinding constrBinding = BindingResolver.resolveConstructor(visitor.cic);
assertThat(constrBinding).isNotNull();
assertThat(constrBinding.isConstructor()).isTrue();
assertThat(visitor.mi).isNotNull();
IMethodBinding methodBinding = BindingResolver.resolveMethod(visitor.mi);
assertThat(methodBinding).isNotNull();
assertThat(methodBinding.getName()).isEqualTo("run");
}
}

View File

@@ -0,0 +1,69 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
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 ControlFlowGraphTest {
@Test
void testSequentialAndBranchingCfg(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run(boolean flag) {\n" +
" int a = 1;\n" +
" if (flag) {\n" +
" a = 2;\n" +
" } else {\n" +
" a = 3;\n" +
" }\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
assertThat(cfg).isNotNull();
assertThat(cfg.getEntryNode().getSuccessors()).hasSize(1);
// Entry goes to first statement: 'int a = 1;'
ControlFlowGraph.CfgNode firstStmtNode = cfg.getEntryNode().getSuccessors().iterator().next();
assertThat(firstStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
// First stmt goes to condition of 'if (flag)'
assertThat(firstStmtNode.getSuccessors()).hasSize(1);
ControlFlowGraph.CfgNode condNode = firstStmtNode.getSuccessors().iterator().next();
assertThat(condNode.getAstNode()).isInstanceOf(SimpleName.class); // flag
// Cond node goes to two branches: 'a = 2;' and 'a = 3;'
assertThat(condNode.getSuccessors()).hasSize(2);
for (ControlFlowGraph.CfgNode branchSuccessor : condNode.getSuccessors()) {
assertThat(branchSuccessor.getAstNode()).isInstanceOf(ExpressionStatement.class); // assignment expression statement
// Each branch successor should flow into the next sequential statement: 'int b = a;'
assertThat(branchSuccessor.getSuccessors()).hasSize(1);
ControlFlowGraph.CfgNode nextStmtNode = branchSuccessor.getSuccessors().iterator().next();
assertThat(nextStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
assertThat(nextStmtNode.getSuccessors()).contains(cfg.getExitNode());
}
}
}

View File

@@ -0,0 +1,91 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
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 JdtDataFlowModelTest {
@Test
void testRecursiveJdtDataFlowResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run() {\n" +
" String a = \"VAL1\";\n" +
" String b = a;\n" +
" String c = b;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement cStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment cFrag = (VariableDeclarationFragment) cStmt.fragments().get(0);
Expression initializer = cFrag.getInitializer(); // b
DataFlowModel model = new JdtDataFlowModel(context);
List<Expression> defs = model.getReachingDefinitions(initializer);
assertThat(defs).isNotEmpty();
Expression singleDef = defs.get(defs.size() - 1);
assertThat(singleDef).isInstanceOf(StringLiteral.class);
assertThat(((StringLiteral) singleDef).getLiteralValue()).isEqualTo("VAL1");
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("VAL1");
}
@Test
void testBranchDivergenceJdtDataFlowResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run(boolean flag) {\n" +
" String a = \"SAME\";\n" +
" if (flag) {\n" +
" a = \"SAME\";\n" +
" } else {\n" +
" a = \"SAME\";\n" +
" }\n" +
" String b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer(); // a
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
// Both branches assign "SAME", so it converges to "SAME"
assertThat(val).isEqualTo("SAME");
}
}

View File

@@ -0,0 +1,56 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import org.eclipse.jdt.core.dom.*;
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 LegacyDataFlowModelTest {
@Test
void testLegacyDataFlowResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run() {\n" +
" String a = \"VAL1\";\n" +
" String b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment frag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = frag.getInitializer(); // 'a' reference
VariableTracer tracer = new VariableTracer(context, new ConstantResolver());
DataFlowModel model = new LegacyDataFlowModel(tracer);
List<Expression> defs = model.getReachingDefinitions(initializer);
assertThat(defs).isNotEmpty();
Expression resolvedExpr = defs.get(defs.size() - 1);
assertThat(resolvedExpr).isInstanceOf(StringLiteral.class);
assertThat(((StringLiteral) resolvedExpr).getLiteralValue()).isEqualTo("VAL1");
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("VAL1");
}
}

View File

@@ -0,0 +1,105 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
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.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
class ReachingDefinitionsTest {
@Test
void testReachingDefinitionsBranchMerge(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run(boolean flag) {\n" +
" int a = 1;\n" +
" if (flag) {\n" +
" a = 2;\n" +
" } else {\n" +
" a = 3;\n" +
" }\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
ReachingDefinitions rd = new ReachingDefinitions(cfg);
// Find usage 'a' in 'int b = a;'
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer(); // simpleName 'a'
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
assertThat(defs).hasSize(2);
Set<String> values = defs.stream()
.map(ReachingDefinitions::getDefinitionExpression)
.map(expr -> {
if (expr instanceof NumberLiteral nl) {
return nl.getToken();
}
return expr.toString();
})
.collect(Collectors.toSet());
assertThat(values).containsExactlyInAnyOrder("2", "3");
}
@Test
void testReachingDefinitionsKilled(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run() {\n" +
" int a = 1;\n" +
" a = 2;\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
ReachingDefinitions rd = new ReachingDefinitions(cfg);
// Find usage 'a' in 'int b = a;'
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer();
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
assertThat(defs).hasSize(1);
ASTNode singleDef = defs.iterator().next();
Expression defExpr = ReachingDefinitions.getDefinitionExpression(singleDef);
assertThat(defExpr).isInstanceOf(NumberLiteral.class);
assertThat(((NumberLiteral) defExpr).getToken()).isEqualTo("2");
}
}