From ee98c3dbb4c4cf0cb0b411914fc758ccdf00d038 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Fri, 10 Oct 2025 10:43:16 +0200 Subject: [PATCH] Add flow functions that support method chaining --- .../ChainingBackwardFlowFunction.java | 65 +++++++++++ .../ChainingFlowFunctionFactory.java | 75 ++++++++++++ .../ChainingForwardFlowFunction.java | 68 +++++++++++ idealPDS/src/test/java/chains/Chain.java | 26 +++++ .../test/java/chains/ChainStateMachine.java | 101 ++++++++++++++++ .../ChainingTestFlowFunctionFactory.java | 29 +++++ .../test/java/chains/MethodChainingTest.java | 108 ++++++++++++++++++ .../java/test/IDEALTestRunnerInterceptor.java | 35 +++++- .../test/java/test/IDEALTestingFramework.java | 20 ++-- idealPDS/src/test/java/test/TestConfig.java | 7 ++ 10 files changed, 522 insertions(+), 12 deletions(-) create mode 100644 idealPDS/src/main/java/typestate/ChainingBackwardFlowFunction.java create mode 100644 idealPDS/src/main/java/typestate/ChainingFlowFunctionFactory.java create mode 100644 idealPDS/src/main/java/typestate/ChainingForwardFlowFunction.java create mode 100644 idealPDS/src/test/java/chains/Chain.java create mode 100644 idealPDS/src/test/java/chains/ChainStateMachine.java create mode 100644 idealPDS/src/test/java/chains/ChainingTestFlowFunctionFactory.java create mode 100644 idealPDS/src/test/java/chains/MethodChainingTest.java diff --git a/idealPDS/src/main/java/typestate/ChainingBackwardFlowFunction.java b/idealPDS/src/main/java/typestate/ChainingBackwardFlowFunction.java new file mode 100644 index 00000000..e430e2e4 --- /dev/null +++ b/idealPDS/src/main/java/typestate/ChainingBackwardFlowFunction.java @@ -0,0 +1,65 @@ +/** + * ***************************************************************************** + * Copyright (c) 2018 Fraunhofer IEM, Paderborn, Germany + *

+ * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + *

+ * SPDX-License-Identifier: EPL-2.0 + *

+ * Contributors: + * Johannes Spaeth - initial API and implementation + * ***************************************************************************** + */ +package typestate; + +import boomerang.flowfunction.DefaultBackwardFlowFunction; +import boomerang.options.IAllocationSite; +import boomerang.scope.ControlFlowGraph; +import boomerang.scope.DeclaredMethod; +import boomerang.scope.InvokeExpr; +import boomerang.scope.Statement; +import boomerang.scope.Val; +import boomerang.solver.Strategies; +import boomerang.utils.MethodWrapper; +import java.util.Collection; +import java.util.LinkedHashSet; +import sync.pds.solver.nodes.Node; +import wpds.interfaces.State; + +public class ChainingBackwardFlowFunction extends DefaultBackwardFlowFunction { + + private final Collection methodChains; + + public ChainingBackwardFlowFunction( + IAllocationSite allocationSite, + Strategies strategies, + Collection methodChains) { + super(allocationSite, strategies); + + this.methodChains = methodChains; + } + + @Override + public Collection callToReturnFlow( + ControlFlowGraph.Edge currEdge, ControlFlowGraph.Edge nextEdge, Val fact) { + Collection out = new LinkedHashSet<>(super.callToReturnFlow(currEdge, nextEdge, fact)); + + Statement statement = nextEdge.getTarget(); + if (statement.isAssignStmt() && statement.containsInvokeExpr()) { + InvokeExpr invokeExpr = statement.getInvokeExpr(); + DeclaredMethod declaredMethod = invokeExpr.getDeclaredMethod(); + + if (methodChains.contains(declaredMethod.toMethodWrapper())) { + Val leftOp = statement.getLeftOp(); + + if (leftOp.equals(fact) && invokeExpr.isInstanceInvokeExpr()) { + out.add(new Node<>(nextEdge, invokeExpr.getBase())); + } + } + } + + return out; + } +} diff --git a/idealPDS/src/main/java/typestate/ChainingFlowFunctionFactory.java b/idealPDS/src/main/java/typestate/ChainingFlowFunctionFactory.java new file mode 100644 index 00000000..df2e5fa6 --- /dev/null +++ b/idealPDS/src/main/java/typestate/ChainingFlowFunctionFactory.java @@ -0,0 +1,75 @@ +/** + * ***************************************************************************** + * Copyright (c) 2018 Fraunhofer IEM, Paderborn, Germany + *

+ * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + *

+ * SPDX-License-Identifier: EPL-2.0 + *

+ * Contributors: + * Johannes Spaeth - initial API and implementation + * ***************************************************************************** + */ +package typestate; + +import boomerang.flowfunction.DefaultFlowFunctionFactory; +import boomerang.flowfunction.IBackwardFlowFunction; +import boomerang.flowfunction.IForwardFlowFunction; +import boomerang.options.BoomerangOptions; +import boomerang.scope.FrameworkScope; +import boomerang.solver.BackwardBoomerangSolver; +import boomerang.solver.ForwardBoomerangSolver; +import boomerang.solver.Strategies; +import boomerang.utils.MethodWrapper; +import java.util.Collection; + +/** + * Flow function factory that extends the {@link DefaultFlowFunctionFactory} by adding features to + * deal with chained methods when applying the call-to-return flow. Intermediate representations + * transform chained method calls into multiple statements and introduce new locals for each + * intermediate call. Although the chained calls return the original objects (i.e. an alias), the + * default flow function do not find the aliases. The extension in this flow function factory adds + * the functionality to collect corresponding aliases, too. + * + *

For example, a program + * + *

{@code
+ * l.chain().chain();
+ * }
+ * + * is transformed into the intermediate representation + * + *
{@code
+ * $s0 = l.chain();
+ * $s1 = $s0.chain();
+ * }
+ * + * The extended flow functions make sure to collect $s0 and $s1 as alias s.t. the analysis can + * collect the second call to chain(). + */ +public class ChainingFlowFunctionFactory extends DefaultFlowFunctionFactory { + + private final Collection methodChains; + + public ChainingFlowFunctionFactory(Collection methodChains) { + this.methodChains = methodChains; + } + + @Override + public IForwardFlowFunction createForwardFlowFunction( + FrameworkScope frameworkScope, BoomerangOptions options, ForwardBoomerangSolver solver) { + Strategies strategies = createStrategies(frameworkScope, options, solver); + + return new ChainingForwardFlowFunction(strategies, methodChains); + } + + @Override + public IBackwardFlowFunction createBackwardFlowFunction( + FrameworkScope frameworkScope, BoomerangOptions options, BackwardBoomerangSolver solver) { + Strategies strategies = createStrategies(frameworkScope, options, solver); + + return new ChainingBackwardFlowFunction(options.allocationSite(), strategies, methodChains); + } +} diff --git a/idealPDS/src/main/java/typestate/ChainingForwardFlowFunction.java b/idealPDS/src/main/java/typestate/ChainingForwardFlowFunction.java new file mode 100644 index 00000000..afe66ff6 --- /dev/null +++ b/idealPDS/src/main/java/typestate/ChainingForwardFlowFunction.java @@ -0,0 +1,68 @@ +/** + * ***************************************************************************** + * Copyright (c) 2018 Fraunhofer IEM, Paderborn, Germany + *

+ * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + *

+ * SPDX-License-Identifier: EPL-2.0 + *

+ * Contributors: + * Johannes Spaeth - initial API and implementation + * ***************************************************************************** + */ +package typestate; + +import boomerang.ForwardQuery; +import boomerang.flowfunction.DefaultForwardFlowFunction; +import boomerang.scope.ControlFlowGraph; +import boomerang.scope.DeclaredMethod; +import boomerang.scope.InvokeExpr; +import boomerang.scope.Statement; +import boomerang.scope.Val; +import boomerang.solver.Strategies; +import boomerang.utils.MethodWrapper; +import java.util.Collection; +import java.util.LinkedHashSet; +import sync.pds.solver.nodes.Node; +import wpds.interfaces.State; + +public class ChainingForwardFlowFunction extends DefaultForwardFlowFunction { + + private final Collection methodChains; + + public ChainingForwardFlowFunction( + Strategies strategies, Collection methodChains) { + super(strategies); + + this.methodChains = methodChains; + } + + @Override + public Collection callToReturnFlow( + ForwardQuery query, ControlFlowGraph.Edge edge, Val fact) { + Collection out = new LinkedHashSet<>(super.callToReturnFlow(query, edge, fact)); + + Statement statement = edge.getStart(); + if (statement.isAssignStmt() && statement.containsInvokeExpr()) { + InvokeExpr invokeExpr = statement.getInvokeExpr(); + DeclaredMethod declaredMethod = invokeExpr.getDeclaredMethod(); + + if (methodChains.contains(declaredMethod.toMethodWrapper())) { + /* If the current statement is a chained method call, consider it as an alias and + * continue the propagation with the implicitly defined local + */ + if (invokeExpr.isInstanceInvokeExpr()) { + Val base = invokeExpr.getBase(); + + if (base.equals(fact)) { + out.add(new Node<>(edge, statement.getLeftOp())); + } + } + } + } + + return out; + } +} diff --git a/idealPDS/src/test/java/chains/Chain.java b/idealPDS/src/test/java/chains/Chain.java new file mode 100644 index 00000000..2fd42baf --- /dev/null +++ b/idealPDS/src/test/java/chains/Chain.java @@ -0,0 +1,26 @@ +/** + * ***************************************************************************** + * Copyright (c) 2018 Fraunhofer IEM, Paderborn, Germany + *

+ * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + *

+ * SPDX-License-Identifier: EPL-2.0 + *

+ * Contributors: + * Johannes Spaeth - initial API and implementation + * ***************************************************************************** + */ +package chains; + +public class Chain { + + public Chain chain1() { + return this; + } + + public Chain chain2() { + return this; + } +} diff --git a/idealPDS/src/test/java/chains/ChainStateMachine.java b/idealPDS/src/test/java/chains/ChainStateMachine.java new file mode 100644 index 00000000..5a616dcd --- /dev/null +++ b/idealPDS/src/test/java/chains/ChainStateMachine.java @@ -0,0 +1,101 @@ +/** + * ***************************************************************************** + * Copyright (c) 2018 Fraunhofer IEM, Paderborn, Germany + *

+ * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + *

+ * SPDX-License-Identifier: EPL-2.0 + *

+ * Contributors: + * Johannes Spaeth - initial API and implementation + * ***************************************************************************** + */ +package chains; + +import boomerang.WeightedForwardQuery; +import boomerang.scope.ControlFlowGraph; +import java.util.Collection; +import java.util.Collections; +import typestate.TransitionFunction; +import typestate.finiteautomata.MatcherTransition; +import typestate.finiteautomata.State; +import typestate.finiteautomata.TypeStateMachineWeightFunctions; + +public class ChainStateMachine extends TypeStateMachineWeightFunctions { + + public enum States implements State { + INIT, + CHAIN1, + CHAIN2; + + @Override + public boolean isErrorState() { + return this != CHAIN2; + } + + @Override + public boolean isInitialState() { + return this == INIT; + } + + @Override + public boolean isAccepting() { + return this == CHAIN2; + } + } + + public ChainStateMachine() { + addTransition( + new MatcherTransition( + States.INIT, + ".*chain1.*", + MatcherTransition.Parameter.This, + States.CHAIN1, + MatcherTransition.Type.OnCallToReturn)); + addTransition( + new MatcherTransition( + States.CHAIN1, + ".chain1.*", + MatcherTransition.Parameter.This, + States.CHAIN1, + MatcherTransition.Type.OnCallToReturn)); + addTransition( + new MatcherTransition( + States.CHAIN1, + ".*chain2.*", + MatcherTransition.Parameter.This, + States.CHAIN2, + MatcherTransition.Type.OnCallToReturn)); + addTransition( + new MatcherTransition( + States.CHAIN2, + ".chain1.*", + MatcherTransition.Parameter.This, + States.CHAIN1, + MatcherTransition.Type.OnCallToReturn)); + addTransition( + new MatcherTransition( + States.CHAIN2, + ".chain2.*", + MatcherTransition.Parameter.This, + States.CHAIN2, + MatcherTransition.Type.OnCallToReturn)); + } + + @Override + public Collection> generateSeed( + ControlFlowGraph.Edge stmt) { + try { + return generateAtAllocationSiteOf(stmt, Class.forName(Chain.class.getName())); + } catch (ClassNotFoundException e) { + return Collections.emptySet(); + } + } + + @Override + protected State initialState() { + return States.INIT; + } +} diff --git a/idealPDS/src/test/java/chains/ChainingTestFlowFunctionFactory.java b/idealPDS/src/test/java/chains/ChainingTestFlowFunctionFactory.java new file mode 100644 index 00000000..1979b447 --- /dev/null +++ b/idealPDS/src/test/java/chains/ChainingTestFlowFunctionFactory.java @@ -0,0 +1,29 @@ +/** + * ***************************************************************************** + * Copyright (c) 2018 Fraunhofer IEM, Paderborn, Germany + *

+ * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + *

+ * SPDX-License-Identifier: EPL-2.0 + *

+ * Contributors: + * Johannes Spaeth - initial API and implementation + * ***************************************************************************** + */ +package chains; + +import boomerang.utils.MethodWrapper; +import java.util.Set; +import typestate.ChainingFlowFunctionFactory; + +public class ChainingTestFlowFunctionFactory extends ChainingFlowFunctionFactory { + + public ChainingTestFlowFunctionFactory() { + super( + Set.of( + new MethodWrapper(Chain.class.getName(), "chain1", Chain.class.getName()), + new MethodWrapper(Chain.class.getName(), "chain2", Chain.class.getName()))); + } +} diff --git a/idealPDS/src/test/java/chains/MethodChainingTest.java b/idealPDS/src/test/java/chains/MethodChainingTest.java new file mode 100644 index 00000000..bd0e9758 --- /dev/null +++ b/idealPDS/src/test/java/chains/MethodChainingTest.java @@ -0,0 +1,108 @@ +/** + * ***************************************************************************** + * Copyright (c) 2018 Fraunhofer IEM, Paderborn, Germany + *

+ * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + *

+ * SPDX-License-Identifier: EPL-2.0 + *

+ * Contributors: + * Johannes Spaeth - initial API and implementation + * ***************************************************************************** + */ +package chains; + +import assertions.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import test.IDEALTestRunnerInterceptor; +import test.TestConfig; +import test.TestParameters; + +@ExtendWith(IDEALTestRunnerInterceptor.class) +@TestConfig( + stateMachine = ChainStateMachine.class, + excludedClasses = {Chain.class}, + flowFunctions = TestConfig.FlowFunctions.CHAINING) +public class MethodChainingTest { + + @Test + @TestParameters(expectedSeedCount = 1, expectedAssertionCount = 1) + public void noChainingTest1() { + Chain chain = new Chain(); + chain.chain1(); + + Assertions.mustBeInErrorState(chain); + } + + @Test + @TestParameters(expectedSeedCount = 1, expectedAssertionCount = 1) + public void chainingTest1() { + Chain chain = new Chain(); + chain.chain1(); + + Assertions.mustBeInErrorState(chain); + } + + @Test + @TestParameters(expectedSeedCount = 1, expectedAssertionCount = 1) + public void noChainingTest2() { + Chain chain = new Chain(); + chain.chain1(); + chain.chain2(); + + Assertions.mustBeInAcceptingState(chain); + } + + @Test + @TestParameters(expectedSeedCount = 1, expectedAssertionCount = 1) + public void chainingTest2() { + Chain chain = new Chain(); + chain.chain1().chain2(); + + Assertions.mustBeInAcceptingState(chain); + } + + @Test + @TestParameters(expectedSeedCount = 1, expectedAssertionCount = 1) + public void noChainingTest3() { + Chain chain = new Chain(); + chain.chain1(); + chain.chain1(); + chain.chain2(); + + Assertions.mustBeInAcceptingState(chain); + } + + @Test + @TestParameters(expectedSeedCount = 1, expectedAssertionCount = 1) + public void chainingTest3() { + Chain chain = new Chain(); + chain.chain1().chain1().chain2(); + + Assertions.mustBeInAcceptingState(chain); + } + + @Test + @TestParameters(expectedSeedCount = 1, expectedAssertionCount = 1) + public void noChainingTest4() { + Chain chain = new Chain(); + chain.chain1(); + chain.chain1(); + chain.chain2(); + chain.chain1(); + + Assertions.mustBeInErrorState(chain); + } + + @Test + @TestParameters(expectedSeedCount = 1, expectedAssertionCount = 1) + public void chainingTest4() { + Chain chain = new Chain(); + chain.chain1().chain1().chain2().chain1(); + + Assertions.mustBeInErrorState(chain); + } +} diff --git a/idealPDS/src/test/java/test/IDEALTestRunnerInterceptor.java b/idealPDS/src/test/java/test/IDEALTestRunnerInterceptor.java index a9100233..ff3db30f 100644 --- a/idealPDS/src/test/java/test/IDEALTestRunnerInterceptor.java +++ b/idealPDS/src/test/java/test/IDEALTestRunnerInterceptor.java @@ -14,6 +14,11 @@ */ package test; +import boomerang.flowfunction.DefaultFlowFunctionFactory; +import boomerang.flowfunction.IFlowFunctionFactory; +import boomerang.options.BoomerangOptions; +import boomerang.solver.Strategies; +import chains.ChainingTestFlowFunctionFactory; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; @@ -32,6 +37,7 @@ public class IDEALTestRunnerInterceptor implements BeforeAllCallback, InvocationInterceptor, AfterEachCallback { private IDEALTestingFramework testingFramework; + private BoomerangOptions options; @Override public void beforeAll(ExtensionContext context) { @@ -78,6 +84,7 @@ public void beforeAll(ExtensionContext context) { .map(Class::getName) .collect(Collectors.toList()); testingFramework = new IDEALTestingFramework(stateMachine, includedClasses, excludedClasses); + options = createOptions(testConfig); } @Override @@ -97,7 +104,7 @@ public void interceptTestMethod( + "' in class '" + testClassName + "' is not annotated with '" - + TestConfig.class.getSimpleName() + + TestParameters.class.getSimpleName() + "'"); } @@ -111,12 +118,12 @@ public void interceptTestMethod( testClassName, testMethodName, parameters.expectedSeedCount(), - parameters.expectedAssertionCount()); + parameters.expectedAssertionCount(), + options); try { invocation.proceed(); } catch (Throwable ignored) { - } } @@ -124,4 +131,26 @@ public void interceptTestMethod( public void afterEach(ExtensionContext context) { testingFramework.cleanUp(); } + + private BoomerangOptions createOptions(TestConfig config) { + IFlowFunctionFactory factory = getFlowFunctionFactory(config.flowFunctions()); + + return BoomerangOptions.builder() + .withFlowFunctionFactory(factory) + .withStaticFieldStrategy(Strategies.StaticFieldStrategy.FLOW_SENSITIVE) + .withAnalysisTimeout(-1) + .enableAllowMultipleQueries(true) + .build(); + } + + private IFlowFunctionFactory getFlowFunctionFactory(TestConfig.FlowFunctions flowFunctions) { + switch (flowFunctions) { + case DEFAULT: + return new DefaultFlowFunctionFactory(); + case CHAINING: + return new ChainingTestFlowFunctionFactory(); + default: + throw new RuntimeException("Unknown FlowFunctions: " + flowFunctions); + } + } } diff --git a/idealPDS/src/test/java/test/IDEALTestingFramework.java b/idealPDS/src/test/java/test/IDEALTestingFramework.java index c5a4c4d1..b7234009 100644 --- a/idealPDS/src/test/java/test/IDEALTestingFramework.java +++ b/idealPDS/src/test/java/test/IDEALTestingFramework.java @@ -30,7 +30,6 @@ import boomerang.scope.Method; import boomerang.scope.Statement; import boomerang.scope.Val; -import boomerang.solver.Strategies; import boomerang.utils.MethodWrapper; import ideal.IDEALAnalysis; import ideal.IDEALAnalysisDefinition; @@ -61,7 +60,11 @@ public IDEALTestingFramework( } public void analyze( - String targetClassName, String targetMethodName, int expectedSeeds, int expectedAssertions) { + String targetClassName, + String targetMethodName, + int expectedSeeds, + int expectedAssertions, + BoomerangOptions options) { LOGGER.info( "Running '{}' in class '{}' with {} assertions", targetMethodName, @@ -87,7 +90,8 @@ public void analyze( // Run IDEal StoreIDEALResultHandler resultHandler = new StoreIDEALResultHandler<>(); - IDEALAnalysis idealAnalysis = createAnalysis(frameworkScope, resultHandler); + IDEALAnalysis idealAnalysis = + createAnalysis(frameworkScope, resultHandler, options); idealAnalysis.run(); // Update results @@ -107,7 +111,9 @@ public void analyze( } protected IDEALAnalysis createAnalysis( - FrameworkScope frameworkScope, StoreIDEALResultHandler resultHandler) { + FrameworkScope frameworkScope, + StoreIDEALResultHandler resultHandler, + BoomerangOptions options) { return new IDEALAnalysis<>( new IDEALAnalysisDefinition<>() { @@ -131,11 +137,7 @@ public IDEALResultHandler getResultHandler() { @Override public BoomerangOptions boomerangOptions() { - return BoomerangOptions.builder() - .withStaticFieldStrategy(Strategies.StaticFieldStrategy.FLOW_SENSITIVE) - .withAnalysisTimeout(-1) - .enableAllowMultipleQueries(true) - .build(); + return options; } @Override diff --git a/idealPDS/src/test/java/test/TestConfig.java b/idealPDS/src/test/java/test/TestConfig.java index be652305..a5f6ef1e 100644 --- a/idealPDS/src/test/java/test/TestConfig.java +++ b/idealPDS/src/test/java/test/TestConfig.java @@ -23,9 +23,16 @@ @Target(ElementType.TYPE) public @interface TestConfig { + enum FlowFunctions { + DEFAULT, + CHAINING + } + Class stateMachine(); Class[] includedClasses() default {}; Class[] excludedClasses() default {}; + + FlowFunctions flowFunctions() default FlowFunctions.DEFAULT; }