diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/StringAnalysisReflectiveCalls.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/StringAnalysisReflectiveCalls.scala
new file mode 100644
index 0000000000..4121a0d735
--- /dev/null
+++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/StringAnalysisReflectiveCalls.scala
@@ -0,0 +1,338 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package support
+package info
+
+import scala.annotation.switch
+
+import java.net.URL
+import scala.collection.mutable.ListBuffer
+import scala.util.Try
+
+import org.opalj.br.DeclaredMethod
+import org.opalj.br.ObjectType
+import org.opalj.br.ReferenceType
+import org.opalj.br.analyses.BasicReport
+import org.opalj.br.analyses.DeclaredMethods
+import org.opalj.br.analyses.DeclaredMethodsKey
+import org.opalj.br.analyses.Project
+import org.opalj.br.analyses.ProjectAnalysisApplication
+import org.opalj.br.analyses.ReportableAnalysisResult
+import org.opalj.br.fpcf.ContextProviderKey
+import org.opalj.br.fpcf.FPCFAnalysesManagerKey
+import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
+import org.opalj.br.fpcf.PropertyStoreKey
+import org.opalj.br.fpcf.analyses.ContextProvider
+import org.opalj.br.fpcf.properties.string.StringConstancyLevel
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.br.instructions.Instruction
+import org.opalj.br.instructions.INVOKESTATIC
+import org.opalj.br.instructions.INVOKEVIRTUAL
+import org.opalj.fpcf.FinalEP
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.Assignment
+import org.opalj.tac.Call
+import org.opalj.tac.ComputeTACAIKey
+import org.opalj.tac.ExprStmt
+import org.opalj.tac.StaticFunctionCall
+import org.opalj.tac.Stmt
+import org.opalj.tac.TACMethodParameter
+import org.opalj.tac.TACode
+import org.opalj.tac.V
+import org.opalj.tac.VirtualFunctionCall
+import org.opalj.tac.cg.RTACallGraphKey
+import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis
+import org.opalj.tac.fpcf.analyses.fieldaccess.reflection.ReflectionRelatedFieldAccessesAnalysisScheduler
+import org.opalj.tac.fpcf.analyses.string.LazyStringAnalysis
+import org.opalj.tac.fpcf.analyses.string.VariableContext
+import org.opalj.tac.fpcf.analyses.string.flowanalysis.LazyMethodStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l0.LazyL0StringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l1.LazyL1StringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l2.LazyL2StringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l3.LazyL3StringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.trivial.LazyTrivialStringAnalysis
+import org.opalj.tac.fpcf.analyses.systemproperties.TriggeredSystemPropertiesAnalysisScheduler
+import org.opalj.util.PerformanceEvaluation.time
+
+/**
+ * Analyzes a project for calls provided by the Java Reflection API and tries to determine which string values are /
+ * could be passed to these calls. Includes an option to also analyze relevant JavaX Crypto API calls.
+ *
+ * @author Maximilian Rüsch
+ */
+object StringAnalysisReflectiveCalls extends ProjectAnalysisApplication {
+
+ /**
+ * @param detectedValues Stores a list of pairs where the first element corresponds to the entities passed to the analysis and the second
+ * element corresponds to the method name in which the entity occurred, i.e. a value in [[relevantMethodNames]].
+ */
+ private case class State(detectedValues: ListBuffer[(VariableContext, String)])
+
+ private case class Configuration(
+ includeCrypto: Boolean,
+ private val analysisConfig: Configuration.AnalysisConfig
+ ) {
+
+ def analyses: Seq[FPCFLazyAnalysisScheduler] = {
+ analysisConfig match {
+ case Configuration.TrivialAnalysis => Seq(LazyTrivialStringAnalysis)
+ case Configuration.LevelAnalysis(level) =>
+ Seq(
+ LazyStringAnalysis,
+ LazyMethodStringFlowAnalysis,
+ Configuration.LevelToSchedulerMapping(level)
+ )
+ }
+ }
+ }
+
+ private object Configuration {
+
+ private[Configuration] trait AnalysisConfig
+ private[Configuration] case object TrivialAnalysis extends AnalysisConfig
+ private[Configuration] case class LevelAnalysis(level: Int) extends AnalysisConfig
+
+ final val LevelToSchedulerMapping = Map(
+ 0 -> LazyL0StringFlowAnalysis,
+ 1 -> LazyL1StringFlowAnalysis,
+ 2 -> LazyL2StringFlowAnalysis,
+ 3 -> LazyL3StringFlowAnalysis
+ )
+
+ def apply(parameters: Seq[String]): Configuration = {
+ val includeCrypto = parameters.contains("-includeCryptoApi")
+ val levelParameter = parameters.find(_.startsWith("-level=")).getOrElse("-level=trivial")
+ val analysisConfig = levelParameter.replace("-level=", "") match {
+ case "trivial" => TrivialAnalysis
+ case string => LevelAnalysis(string.toInt)
+ }
+
+ new Configuration(includeCrypto, analysisConfig)
+ }
+ }
+
+ private val relevantCryptoMethodNames = List(
+ "javax.crypto.Cipher#getInstance",
+ "javax.crypto.Cipher#getMaxAllowedKeyLength",
+ "javax.crypto.Cipher#getMaxAllowedParameterSpec",
+ "javax.crypto.Cipher#unwrap",
+ "javax.crypto.CipherSpi#engineSetMode",
+ "javax.crypto.CipherSpi#engineSetPadding",
+ "javax.crypto.CipherSpi#engineUnwrap",
+ "javax.crypto.EncryptedPrivateKeyInfo#getKeySpec",
+ "javax.crypto.ExemptionMechanism#getInstance",
+ "javax.crypto.KeyAgreement#getInstance",
+ "javax.crypto.KeyGenerator#getInstance",
+ "javax.crypto.Mac#getInstance",
+ "javax.crypto.SealedObject#getObject",
+ "javax.crypto.SecretKeyFactory#getInstance"
+ )
+
+ private val relevantReflectionMethodNames = List(
+ "java.lang.Class#forName",
+ "java.lang.ClassLoader#loadClass",
+ "java.lang.Class#getField",
+ "java.lang.Class#getDeclaredField",
+ "java.lang.Class#getMethod",
+ "java.lang.Class#getDeclaredMethod"
+ )
+
+ override def title: String = "String Analysis for Reflective Calls"
+
+ override def description: String = {
+ "Finds calls to methods provided by the Java Reflection API and tries to resolve passed string values"
+ }
+
+ override def analysisSpecificParametersDescription: String =
+ s"""
+ | [-includeCryptoApi]
+ | [-level=trivial|${Configuration.LevelToSchedulerMapping.keys.toSeq.sorted.mkString("|")}]
+ |""".stripMargin
+
+ override def checkAnalysisSpecificParameters(parameters: Seq[String]): Iterable[String] = {
+ parameters.flatMap {
+ case "-includeCryptoApi" => None
+ case levelParameter if levelParameter.startsWith("-level=") =>
+ levelParameter.replace("-level=", "") match {
+ case "trivial" =>
+ None
+ case string
+ if Try(string.toInt).isSuccess
+ && Configuration.LevelToSchedulerMapping.keySet.contains(string.toInt) =>
+ None
+ case value =>
+ Some(s"Unknown level parameter value: $value")
+ }
+
+ case param => Some(s"unknown parameter: $param")
+ }
+ }
+
+ /**
+ * Retrieves all relevant method names, i.e., those methods from the Reflection API that have at least one string
+ * argument and shall be considered by this analysis. The string are supposed to have the format as produced
+ * by [[buildFQMethodName]]. If the 'crypto' parameter is set, relevant methods of the javax.crypto API are
+ * included, too.
+ */
+ private def relevantMethodNames(implicit configuration: Configuration) = {
+ if (configuration.includeCrypto)
+ relevantReflectionMethodNames ++ relevantCryptoMethodNames
+ else
+ relevantReflectionMethodNames
+ }
+
+ /**
+ * Using a `declaringClass` and a `methodName`, this function returns a formatted version of the
+ * fully-qualified method name, in the format [fully-qualified class name]#[method name]
+ * where the separator for the fq class names is a dot, e.g., "java.lang.Class#forName".
+ */
+ @inline private final def buildFQMethodName(declaringClass: ReferenceType, methodName: String): String =
+ s"${declaringClass.toJava}#$methodName"
+
+ /**
+ * Taking the `declaringClass` and the `methodName` into consideration, this function checks
+ * whether a method is relevant for this analysis.
+ *
+ * @note Internally, this method makes use of [[relevantMethodNames]]. A method can only be
+ * relevant if it occurs in [[relevantMethodNames]].
+ */
+ @inline private final def isRelevantCall(declaringClass: ReferenceType, methodName: String)(
+ implicit configuration: Configuration
+ ): Boolean = relevantMethodNames.contains(buildFQMethodName(declaringClass, methodName))
+
+ /**
+ * Helper function that checks whether an array of [[Instruction]]s contains at least one
+ * relevant method that is to be processed by `doAnalyze`.
+ */
+ private def instructionsContainRelevantMethod(instructions: Array[Instruction])(
+ implicit configuration: Configuration
+ ): Boolean = {
+ instructions
+ .filter(_ != null)
+ .exists {
+ case INVOKESTATIC(declClass, _, methodName, _) if isRelevantCall(declClass, methodName) => true
+ case INVOKEVIRTUAL(declClass, methodName, _) if isRelevantCall(declClass, methodName) => true
+ case _ => false
+ }
+ }
+
+ /**
+ * This function is a wrapper function for processing a method. It checks whether the given
+ * `method`, is relevant at all, and if so uses the given function `call` to call the
+ * analysis using the property store, `ps`, to finally store it in the given `resultMap`.
+ */
+ private def processFunctionCall(pc: Int, dm: DeclaredMethod, call: Call[V])(
+ implicit
+ stmts: Array[Stmt[V]],
+ ps: PropertyStore,
+ contextProvider: ContextProvider,
+ state: State,
+ configuration: Configuration
+ ): Unit = {
+ if (isRelevantCall(call.declaringClass, call.name)) {
+ // Loop through all parameters and start the analysis for those that take a string
+ call.descriptor.parameterTypes.zipWithIndex.foreach {
+ case (ft, index) if ft == ObjectType.String =>
+ val e = VariableContext(
+ pc,
+ call.params(index).asVar.toPersistentForm,
+ contextProvider.newContext(dm)
+ )
+ ps.force(e, StringConstancyProperty.key)
+ state.detectedValues.append((e, buildFQMethodName(call.declaringClass, call.name)))
+ case _ =>
+ }
+ }
+ }
+
+ private def processStatements(tac: TACode[TACMethodParameter, V], dm: DeclaredMethod)(
+ implicit
+ contextProvider: ContextProvider,
+ ps: PropertyStore,
+ state: State,
+ configuration: Configuration
+ ): Unit = {
+ implicit val stmts: Array[Stmt[V]] = tac.stmts
+ stmts.foreach { stmt =>
+ // Using the following switch speeds up the whole process
+ (stmt.astID: @switch) match {
+ case Assignment.ASTID => stmt match {
+ case Assignment(pc, _, c: StaticFunctionCall[V]) =>
+ processFunctionCall(pc, dm, c)
+ case Assignment(pc, _, c: VirtualFunctionCall[V]) =>
+ processFunctionCall(pc, dm, c)
+ case _ =>
+ }
+ case ExprStmt.ASTID => stmt match {
+ case ExprStmt(pc, c: StaticFunctionCall[V]) =>
+ processFunctionCall(pc, dm, c)
+ case ExprStmt(pc, c: VirtualFunctionCall[V]) =>
+ processFunctionCall(pc, dm, c)
+ case _ =>
+ }
+ case _ =>
+ }
+ }
+ }
+
+ override def doAnalyze(
+ project: Project[URL],
+ parameters: Seq[String],
+ isInterrupted: () => Boolean
+ ): ReportableAnalysisResult = {
+ implicit val state: State = State(detectedValues = ListBuffer.empty)
+ implicit val configuration: Configuration = Configuration(parameters)
+
+ val cgKey = RTACallGraphKey
+ val typeIterator = cgKey.getTypeIterator(project)
+ project.updateProjectInformationKeyInitializationData(ContextProviderKey) { _ => typeIterator }
+
+ val manager = project.get(FPCFAnalysesManagerKey)
+ val computeTac = project.get(ComputeTACAIKey)
+ val declaredMethods: DeclaredMethods = project.get(DeclaredMethodsKey)
+ implicit val propertyStore: PropertyStore = project.get(PropertyStoreKey)
+ implicit val contextProvider: ContextProvider = project.get(ContextProviderKey)
+
+ time {
+ manager.runAll(
+ cgKey.allCallGraphAnalyses(project) ++
+ configuration.analyses ++
+ Seq(
+ EagerFieldAccessInformationAnalysis,
+ ReflectionRelatedFieldAccessesAnalysisScheduler,
+ TriggeredSystemPropertiesAnalysisScheduler
+ ),
+ afterPhaseScheduling = _ => {
+ project.allMethodsWithBody.foreach { m =>
+ // To dramatically reduce work, quickly check if a method is relevant at all
+ if (instructionsContainRelevantMethod(m.body.get.instructions)) {
+ processStatements(computeTac(m), declaredMethods(m))
+ }
+ }
+ }
+ )
+ } { t => println(s"Elapsed Time: ${t.toMilliseconds}") }
+
+ val resultMap = Map.from(relevantMethodNames.map((_, ListBuffer.empty[FinalEP[_, StringConstancyProperty]])))
+ state.detectedValues.foreach {
+ case (e, callName) =>
+ resultMap(callName).append(propertyStore(e, StringConstancyProperty.key).asFinal)
+ }
+
+ val report = ListBuffer[String]("Results of the Reflection Analysis:")
+ for ((reflectiveCall, stringTrees) <- resultMap.toSeq.sortBy(_._1)) {
+ val invalidCount = stringTrees.count(_.p.tree.constancyLevel == StringConstancyLevel.Invalid)
+ val constantCount = stringTrees.count(_.p.tree.constancyLevel == StringConstancyLevel.Constant)
+ val partiallyConstantCount =
+ stringTrees.count(_.p.tree.constancyLevel == StringConstancyLevel.PartiallyConstant)
+ val dynamicCount = stringTrees.count(_.p.tree.constancyLevel == StringConstancyLevel.Dynamic)
+
+ report.append(s"$reflectiveCall: ${stringTrees.length}x")
+ report.append(s" -> Invalid: ${invalidCount}x")
+ report.append(s" -> Constant: ${constantCount}x")
+ report.append(s" -> Partially Constant: ${partiallyConstantCount}x")
+ report.append(s" -> Dynamic: ${dynamicCount}x")
+ }
+ BasicReport(report)
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ArrayOps.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ArrayOps.java
new file mode 100644
index 0000000000..9edcc09852
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ArrayOps.java
@@ -0,0 +1,62 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.fixtures.string.tools.StringProvider;
+import org.opalj.fpcf.properties.string.*;
+
+/**
+ * Various tests that test compatibility with array operations (mainly reads with specific array indexes).
+ * Currently, the string analysis does not support array operations and thus fails for every level.
+ *
+ * @see SimpleStringOps
+ */
+public class ArrayOps {
+
+ private String[] monthNames = { "January", "February", "March", getApril() };
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(java.lang.String|java.lang.StringBuilder|java.lang.System|java.lang.Runnable)")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }, reason = "arrays are not supported")
+ public void fromStringArray(int index) {
+ String[] classes = {
+ "java.lang.String", "java.lang.StringBuilder",
+ "java.lang.System", "java.lang.Runnable"
+ };
+ if (index >= 0 && index < classes.length) {
+ analyzeString(classes[index]);
+ }
+ }
+
+ @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW,
+ value = "(java.lang.Object|java.lang.Runtime|java.lang.Integer)")
+ @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH,
+ value = "(java.lang.Object|java.lang.Runtime|java.lang.Integer|.*)")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }, reason = "arrays are not supported")
+ public void arrayStaticAndVirtualFunctionCalls(int i) {
+ String[] classes = {
+ "java.lang.Object",
+ getRuntimeClassName(),
+ StringProvider.getFQClassNameWithStringBuilder("java.lang", "Integer"),
+ System.clearProperty("SomeClass")
+ };
+ analyzeString(classes[i]);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(January|February|March|April)")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }, reason = "arrays are not supported")
+ public void getStringArrayField(int i) {
+ analyzeString(monthNames[i]);
+ }
+
+ private String getRuntimeClassName() {
+ return "java.lang.Runtime";
+ }
+
+ private String getApril() {
+ return "April";
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Complex.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Complex.java
new file mode 100644
index 0000000000..5898f48ca0
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Complex.java
@@ -0,0 +1,126 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.lang.reflect.Method;
+import java.util.Random;
+import java.util.Scanner;
+
+/**
+ * Various tests that test certain complex string analysis scenarios which were either constructed or extracted from the
+ * JDK. Such tests should combine multiple string analysis techniques, the most common are interprocedurality and
+ * control flow sensitivity.
+ *
+ * @see SimpleStringOps
+ */
+public class Complex {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ /**
+ * Taken from com.sun.javafx.property.PropertyReference#reflect.
+ */
+ @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "get-Hello, World-java.lang.Runtime")
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(get-.*|get-Hello, World-java.lang.Runtime)")
+ @Failure(n = 0, levels = Level.L0)
+ @Invalid(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW)
+ @PartiallyConstant(n = 0, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "(get-.*|get-Hello, World-.*)")
+ public void complexDependencyResolve(String s, Class clazz) {
+ String properName = s.length() == 1 ? s.substring(0, 1) :
+ getHelloWorld() + "-" + getRuntimeClassName();
+ String getterName = "get-" + properName;
+ Method m;
+ try {
+ m = clazz.getMethod(getterName);
+ System.out.println(m);
+ analyzeString(getterName);
+ } catch (NoSuchMethodException var13) {
+ }
+ }
+
+ /**
+ * Taken from com.sun.prism.impl.ps.BaseShaderContext#getPaintShader and slightly adapted
+ */
+ @Constant(n = 0, levels = Level.TRUTH, value = "Hello, World_paintName(_PAD|_REFLECT|_REPEAT)?(_AlphaTest)?")
+ @Failure(n = 0, levels = Level.L0)
+ // or-cases are currently not collapsed into simpler conditionals / or-cases using prefix checking
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, value = "((Hello, World_paintName|Hello, World_paintName_PAD|Hello, World_paintName_REFLECT|Hello, World_paintName_REPEAT)_AlphaTest|Hello, World_paintName|Hello, World_paintName_PAD|Hello, World_paintName_REFLECT|Hello, World_paintName_REPEAT)")
+ public void getPaintShader(boolean getPaintType, int spreadMethod, boolean alphaTest) {
+ String shaderName = getHelloWorld() + "_" + "paintName";
+ if (getPaintType) {
+ if (spreadMethod == 0) {
+ shaderName = shaderName + "_PAD";
+ } else if (spreadMethod == 1) {
+ shaderName = shaderName + "_REFLECT";
+ } else if (spreadMethod == 2) {
+ shaderName = shaderName + "_REPEAT";
+ }
+ }
+ if (alphaTest) {
+ shaderName = shaderName + "_AlphaTest";
+ }
+ analyzeString(shaderName);
+ }
+
+ @Failure(n = 0, levels = Level.TRUTH)
+ @Failure(n = 1, levels = Level.TRUTH)
+ public void unknownCharValue() {
+ int charCode = new Random().nextInt(200);
+ char c = (char) charCode;
+ String s = String.valueOf(c);
+ analyzeString(s);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(c);
+ analyzeString(sb.toString());
+ }
+
+ @Failure(n = 0, levels = { Level.L0, Level.L1 })
+ @Constant(n = 0, levels = { Level.L2, Level.L3 }, value = "value")
+ public String cyclicDependencyTest(String s) {
+ String value = getProperty(s);
+ analyzeString(value);
+ return value;
+ }
+
+ /**
+ * Methods are called that return a string but are not within this project => cannot / will not interpret
+ */
+ @Dynamic(n = 0, levels = Level.TRUTH, value = "(.*)*")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 })
+ @Invalid(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW)
+ @Dynamic(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = ".*")
+ public void methodsOutOfScopeTest() throws FileNotFoundException {
+ File file = new File("my-file.txt");
+ Scanner sc = new Scanner(file);
+ StringBuilder sb = new StringBuilder();
+ while (sc.hasNextLine()) {
+ sb.append(sc.nextLine());
+ }
+ analyzeString(sb.toString());
+
+ analyzeString(System.clearProperty("os.version"));
+ }
+
+ private String getProperty(String name) {
+ if (name == null) {
+ return cyclicDependencyTest("default");
+ } else {
+ return "value";
+ }
+ }
+
+ private String getRuntimeClassName() {
+ return "java.lang.Runtime";
+ }
+
+ private static String getHelloWorld() {
+ return "Hello, World";
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ExceptionalControlStructures.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ExceptionalControlStructures.java
new file mode 100644
index 0000000000..1c1f396f3f
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ExceptionalControlStructures.java
@@ -0,0 +1,91 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * Various tests that test general compatibility with the control flow generated by try-catch(-finally) statements.
+ *
+ * Since this type of statement can compile to multiple instances of calls that were initially defined in "finally"
+ * blocks, more sink calls may be generated than visible in the source code.
+ *
+ * @see SimpleStringOps
+ */
+public class ExceptionalControlStructures {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ @Invalid(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW)
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "File Content:.*")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "File Content:")
+ @PartiallyConstant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(File Content:|File Content:.*)")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "File Content:")
+ @PartiallyConstant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(File Content:|File Content:.*)")
+ @Failure(n = 2, levels = Level.L0)
+ public void tryFinally(String filename) {
+ StringBuilder sb = new StringBuilder("File Content:");
+ try {
+ String data = new String(Files.readAllBytes(Paths.get(filename)));
+ sb.append(data);
+ } catch (Exception ignore) {
+ } finally {
+ analyzeString(sb.toString());
+ }
+ }
+
+ @Invalid(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW)
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "=====.*")
+ @Failure(n = 0, levels = Level.L0)
+ // Exception case without own thrown exception
+ @Constant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "==========")
+ @PartiallyConstant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(=====.*=====|==========)")
+ @Failure(n = 1, levels = Level.L0)
+ // The following cases are detected:
+ // 1. Code around Files.readAllBytes failing, throwing a non-exception Throwable -> no append (Pos 1)
+ // 2. Code around Files.readAllBytes failing, throwing an exception Throwable -> exception case append (Pos 4)
+ // 3. First append succeeds, throws no exception -> only first append (Pos 2)
+ // 4. First append is executed but throws an exception Throwable -> both appends (Pos 3)
+ @Constant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "(=====|==========)")
+ @PartiallyConstant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(=====|=====.*|=====.*=====|==========)")
+ @Failure(n = 2, levels = Level.L0)
+ public void tryCatchFinally(String filename) {
+ StringBuilder sb = new StringBuilder("=====");
+ try {
+ String data = new String(Files.readAllBytes(Paths.get(filename)));
+ sb.append(data);
+ } catch (Exception ignore) {
+ sb.append("=====");
+ } finally {
+ analyzeString(sb.toString());
+ }
+ }
+
+ @Invalid(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW)
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "BOS:.*")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "BOS::EOS")
+ @PartiallyConstant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(BOS:.*:EOS|BOS::EOS)")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "BOS::EOS")
+ @PartiallyConstant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(BOS:.*:EOS|BOS::EOS)")
+ @Failure(n = 2, levels = Level.L0)
+ public void tryCatchFinallyWithThrowable(String filename) {
+ StringBuilder sb = new StringBuilder("BOS:");
+ try {
+ String data = new String(Files.readAllBytes(Paths.get(filename)));
+ sb.append(data);
+ } catch (Throwable t) {
+ sb.append(":EOS");
+ } finally {
+ analyzeString(sb.toString());
+ }
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FieldAccesses.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FieldAccesses.java
new file mode 100644
index 0000000000..1a194e32b4
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FieldAccesses.java
@@ -0,0 +1,116 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+import java.util.Random;
+
+/**
+ * Various tests that test general compatibility with the field access information FPCF property, e.g. being able to
+ * analyze field reads and writes across method boundaries.
+ *
+ * @see SimpleStringOps
+ */
+public class FieldAccesses {
+
+ protected String nonFinalNonStaticField = "private l0 non-final string field";
+ public static String nonFinalStaticField = "will not be revealed here";
+ public static final String finalStaticField = "mine";
+ private String fieldWithSelfInit = "init field value";
+ private static final String fieldWithSelfInitWithComplexInit;
+ private String fieldWithConstructorInit;
+ private float fieldWithConstructorParameterInit;
+ private String writeInSameMethodField;
+ private String noWriteField;
+ private Object unsupportedTypeField;
+
+ static {
+ if (new Random().nextBoolean()) {
+ fieldWithSelfInitWithComplexInit = "Impl_Stub_1";
+ } else {
+ fieldWithSelfInitWithComplexInit = "Impl_Stub_2";
+ }
+ }
+
+ public FieldAccesses(float e) {
+ fieldWithConstructorInit = "initialized by constructor";
+ fieldWithConstructorParameterInit = e;
+ }
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "private l0 non-final string field")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 })
+ public void nonFinalFieldRead() {
+ analyzeString(nonFinalNonStaticField);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "will not be revealed here")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 })
+ public void nonFinalStaticFieldRead() {
+ analyzeString(nonFinalStaticField);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "Field Value:mine")
+ @Failure(n = 0, levels = Level.L0)
+ public void publicFinalStaticFieldRead() {
+ StringBuilder sb = new StringBuilder("Field Value:");
+ System.out.println(sb);
+ sb.append(finalStaticField);
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "init field value")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 })
+ public void fieldWithInitRead() {
+ analyzeString(fieldWithSelfInit.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(Impl_Stub_1|Impl_Stub_2)")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 })
+ public void fieldWithInitWithOutOfScopeRead() {
+ analyzeString(fieldWithSelfInitWithComplexInit);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "initialized by constructor")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 })
+ public void fieldInitByConstructorRead() {
+ analyzeString(fieldWithConstructorInit.toString());
+ }
+
+ @Dynamic(n = 0, levels = Level.TRUTH, value = "^-?\\d*\\.{0,1}\\d+$")
+ @Failure(n = 0, levels = Level.L0)
+ @Failure(n = 0, levels = Level.L1, domains = DomainLevel.L1)
+ @Invalid(n = 0, levels = Level.L1, domains = DomainLevel.L2, soundness = SoundnessMode.LOW)
+ @Dynamic(n = 0, levels = Level.L1, domains = DomainLevel.L2, soundness = SoundnessMode.HIGH,
+ value = "^-?\\d*\\.{0,1}\\d+$", reason = "the field value is inlined using L2 domains")
+ @Invalid(n = 0, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW)
+ public void fieldInitByConstructorParameter() {
+ analyzeString(new StringBuilder().append(fieldWithConstructorParameterInit).toString());
+ }
+
+ // Contains a field write in the same method which cannot be captured by flow functions
+ @Constant(n = 0, levels = Level.TRUTH, value = "(some value|^null$)")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 })
+ @Constant(n = 0, levels = Level.L3, soundness = SoundnessMode.LOW, value = "some value")
+ @Dynamic(n = 0, levels = Level.L3, soundness = SoundnessMode.HIGH, value = ".*")
+ public void fieldWriteInSameMethod() {
+ writeInSameMethodField = "some value";
+ analyzeString(writeInSameMethodField);
+ }
+
+ @Dynamic(n = 0, levels = Level.TRUTH, value = "(.*|^null$)")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 })
+ @Constant(n = 0, levels = Level.L3, soundness = SoundnessMode.LOW, value = "^null$")
+ public void fieldWithNoWriteTest() {
+ analyzeString(noWriteField);
+ }
+
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 })
+ public void nonSupportedFieldTypeRead() {
+ analyzeString(unsupportedTypeField.toString());
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionCalls.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionCalls.java
new file mode 100644
index 0000000000..b681b1cd59
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionCalls.java
@@ -0,0 +1,262 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.fixtures.string.tools.StringFactory;
+import org.opalj.fpcf.fixtures.string.tools.ParameterDependentStringFactory;
+import org.opalj.fpcf.fixtures.string.tools.StringProvider;
+import org.opalj.fpcf.properties.string.*;
+
+/**
+ * Various tests that test specific compatibility of the different levels of the string analysis with resolving
+ * different types of function calls and their impact on the analyzed strings. As an example, arbitrary virtual function
+ * calls may be analyzable in one level of the string analysis but not another.
+ *
+ * @see SimpleStringOps
+ */
+public class FunctionCalls {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "java.lang.Object")
+ @Failure(n = 1, levels = Level.L0)
+ public void simpleStringConcatWithStaticFunctionCalls() {
+ analyzeString(StringProvider.concat("java.lang.", "String"));
+ analyzeString(StringProvider.concat("java.", StringProvider.concat("lang.", "Object")));
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.StringBuilder")
+ @Failure(n = 0, levels = { Level.L0, Level.L1 })
+ public void fromFunctionCall() {
+ analyzeString(getStringBuilderClassName());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.StringBuilder")
+ @Failure(n = 0, levels = Level.L0)
+ @Invalid(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW)
+ @PartiallyConstant(n = 0, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "java.lang..*")
+ public void fromConstantAndFunctionCall() {
+ String className = "java.lang.";
+ System.out.println(className);
+ className += getSimpleStringBuilderClassName();
+ analyzeString(className);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.Integer")
+ @Failure(n = 0, levels = Level.L0)
+ public void fromStaticMethodWithParamTest() {
+ analyzeString(StringProvider.getFQClassNameWithStringBuilder("java.lang", "Integer"));
+ }
+
+ @Invalid(n = 0, levels = Level.TRUTH, reason = "the function has no return value, thus it does not return a string")
+ public void functionWithNoReturnValue() {
+ analyzeString(noReturnFunction());
+ }
+
+ /** Belongs to functionWithNoReturnValue. */
+ public static String noReturnFunction() {
+ throw new RuntimeException();
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "Hello, World!")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "Hello, World?")
+ @Failure(n = 1, levels = { Level.L0, Level.L1 })
+ public void functionWithFunctionParameter() {
+ analyzeString(addExclamationMark(getHelloWorld()));
+ analyzeString(addQuestionMark(getHelloWorld()));
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(ERROR|java.lang.Object|java.lang.StringBuilder)")
+ @Constant(n = 0, levels = { Level.L0, Level.L1 }, soundness = SoundnessMode.LOW, value = "ERROR")
+ @Dynamic(n = 0, levels = { Level.L0, Level.L1 }, soundness = SoundnessMode.HIGH, value = "(.*|ERROR)")
+ public void simpleNonVirtualFunctionCallTestWithIf(int i) {
+ String s;
+ if (i == 0) {
+ s = getObjectClassName();
+ } else if (i == 1) {
+ s = getStringBuilderClassName();
+ } else {
+ s = "ERROR";
+ }
+ analyzeString(s);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(ERROR|java.lang.Object|java.lang.StringBuilder)")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW, value = "ERROR")
+ @Dynamic(n = 0, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "(.*|ERROR)")
+ public void initFromNonVirtualFunctionCallTest(int i) {
+ String s;
+ if (i == 0) {
+ s = getObjectClassName();
+ } else if (i == 1) {
+ s = getStringBuilderClassName();
+ } else {
+ s = "ERROR";
+ }
+ StringBuilder sb = new StringBuilder(s);
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "It is (Hello, World|great)")
+ @Failure(n = 0, levels = Level.L0)
+ public void appendWithTwoDefSitesWithFuncCallTest(int i) {
+ String s;
+ if (i > 0) {
+ s = "great";
+ } else {
+ s = getHelloWorld();
+ }
+ analyzeString(new StringBuilder("It is ").append(s).toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "Hello World")
+ @Failure(n = 0, levels = { Level.L0, Level.L1 })
+ public void knownHierarchyInstanceTest() {
+ StringFactory sf = new ParameterDependentStringFactory();
+ analyzeString(sf.getString("World"));
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(Hello|Hello World)")
+ @Failure(n = 0, levels = { Level.L0, Level.L1 })
+ public void unknownHierarchyInstanceTest(StringFactory stringFactory) {
+ analyzeString(stringFactory.getString("World"));
+ }
+
+ /**
+ * A case where the single valid return value of the called function can be resolved without calling the function.
+ */
+ @Constant(n = 0, levels = Level.TRUTH, value = "val")
+ @Failure(n = 0, levels = { Level.L0, Level.L1 }, domains = DomainLevel.L1)
+ @Constant(n = 0, levels = { Level.L2, Level.L3 }, domains = DomainLevel.L1, value = "(One|java.lang.Object|val)")
+ public void resolvableReturnValue() {
+ analyzeString(resolvableReturnValueFunction("val", 42));
+ }
+
+ /**
+ * Belongs to resolvableReturnValue.
+ */
+ private String resolvableReturnValueFunction(String s, int i) {
+ switch (i) {
+ case 0: return getObjectClassName();
+ case 1: return "One";
+ default: return s;
+ }
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(One|java.lang.Object|val)")
+ @Failure(n = 0, levels = { Level.L0, Level.L1 })
+ public void severalReturnValuesTest1() {
+ analyzeString(severalReturnValuesWithSwitchFunction("val", 42));
+ }
+
+ /** Belongs to severalReturnValuesTest1. */
+ private String severalReturnValuesWithSwitchFunction(String s, int i) {
+ switch (i) {
+ case 0: return "One";
+ case 1: return s;
+ default: return getObjectClassName();
+ }
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(Hello, World|that's odd)")
+ @Failure(n = 0, levels = Level.L0)
+ public void severalReturnValuesTest2() {
+ analyzeString(severalReturnValuesWithIfElseFunction(42));
+ }
+
+ /** Belongs to severalReturnValuesTest2. */
+ private static String severalReturnValuesWithIfElseFunction(int i) {
+ // The ternary operator would create only a single "return" statement which is not what we want here
+ if (i % 2 != 0) {
+ return "that's odd";
+ } else {
+ return getHelloWorld();
+ }
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "(Hello, World|my.helper.Class)")
+ @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|Hello, World|my.helper.Class)")
+ @Failure(n = 0, levels = Level.L0)
+ public String calleeWithFunctionParameter(String s, float i) {
+ analyzeString(s);
+ return s;
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "Hello, World")
+ @Failure(n = 0, levels = { Level.L0, Level.L1 })
+ public void firstCallerForCalleeWithFunctionParameter() {
+ String s = calleeWithFunctionParameter(getHelloWorldProxy(), 900);
+ analyzeString(s);
+ }
+
+ public void secondCallerForCalleeWithFunctionParameter() {
+ calleeWithFunctionParameter(getHelperClassProxy(), 900);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "(Hello, World|my.helper.Class)")
+ @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|Hello, World|my.helper.Class)")
+ @Failure(n = 0, levels = Level.L0)
+ public String calleeWithFunctionParameterMultipleCallsInSameMethodTest(String s, float i) {
+ analyzeString(s);
+ return s;
+ }
+
+ public void callerForCalleeWithFunctionParameterMultipleCallsInSameMethodTest() {
+ calleeWithFunctionParameterMultipleCallsInSameMethodTest(getHelloWorldProxy(), 900);
+ calleeWithFunctionParameterMultipleCallsInSameMethodTest(getHelperClassProxy(), 900);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "(string.1|string.2)")
+ @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|string.1|string.2)")
+ public String calleeWithStringParameterMultipleCallsInSameMethodTest(String s, float i) {
+ analyzeString(s);
+ return s;
+ }
+
+ public void callerForCalleeWithStringParameterMultipleCallsInSameMethodTest() {
+ calleeWithStringParameterMultipleCallsInSameMethodTest("string.1", 900);
+ calleeWithStringParameterMultipleCallsInSameMethodTest("string.2", 900);
+ }
+
+ public static String getHelloWorldProxy() {
+ return getHelloWorld();
+ }
+
+ public static String getHelperClassProxy() {
+ return getHelperClass();
+ }
+
+ private static String getHelloWorld() {
+ return "Hello, World";
+ }
+
+ private static String getHelperClass() {
+ return "my.helper.Class";
+ }
+
+ private String getStringBuilderClassName() {
+ return "java.lang.StringBuilder";
+ }
+
+ private String getSimpleStringBuilderClassName() {
+ return "StringBuilder";
+ }
+
+ private String getObjectClassName() {
+ return "java.lang.Object";
+ }
+
+ private static String addExclamationMark(String s) {
+ return s + "!";
+ }
+
+ private String addQuestionMark(String s) {
+ return s + "?";
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionParameter.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionParameter.java
new file mode 100644
index 0000000000..7d4f9d899a
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionParameter.java
@@ -0,0 +1,58 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+/**
+ * Various tests that test whether detection of needing to resolve parameters for a given string works or not. Note that
+ * there is a separate test file for various function calls that also partially covers this detection.
+ *
+ * @see FunctionCalls
+ * @see SimpleStringOps
+ */
+public class FunctionParameter {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ public void parameterCaller() {
+ this.parameterRead("some-param-value", new StringBuilder("some-other-param-value"));
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "some-param-value")
+ @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|some-param-value)",
+ reason = "method is an entry point and thus has callers with unknown context")
+ @Constant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "some-other-param-value")
+ @Dynamic(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|some-other-param-value)",
+ reason = "method is an entry point and thus has callers with unknown context")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "value=some-param-value")
+ @PartiallyConstant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "value=(.*|some-param-value)",
+ reason = "method is an entry point and thus has callers with unknown context")
+ @Failure(n = 2, levels = Level.L0)
+ @Constant(n = 3, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "value=some-param-value-some-other-param-value")
+ @PartiallyConstant(n = 3, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "value=(.*|some-param-value)-(.*|some-other-param-value)",
+ reason = "method is an entry point and thus has callers with unknown context")
+ @Failure(n = 3, levels = Level.L0)
+ public void parameterRead(String stringValue, StringBuilder sbValue) {
+ analyzeString(stringValue);
+ analyzeString(sbValue.toString());
+
+ StringBuilder sb = new StringBuilder("value=");
+ System.out.println(sb.toString());
+ sb.append(stringValue);
+ analyzeString(sb.toString());
+
+ sb.append("-");
+ sb.append(sbValue.toString());
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String")
+ public void noParameterInformationRequiredTest(String s) {
+ System.out.println(s);
+ analyzeString("java.lang.String");
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Loops.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Loops.java
new file mode 100644
index 0000000000..1fb420a1d6
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Loops.java
@@ -0,0 +1,280 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Random;
+
+/**
+ * Various tests that contain some kind of loops which modify string variables, requiring data flow analysis to resolve
+ * these values or at least approximate them. Currently, the string analysis either only interprets the loop body once
+ * (in low-soundness mode) or over-approximates with "any string" (in high-soundness mode).
+ *
+ * @see SimpleStringOps
+ */
+public class Loops {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ /**
+ * Simple for loops with known and unknown bounds. Note that no analysis supports loops yet.
+ */
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "a(b)*")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab")
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*")
+ @PartiallyConstant(n = 1, levels = Level.TRUTH, value = "a(b)*")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab")
+ @Dynamic(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*")
+ public void simpleForLoopWithKnownBounds() {
+ StringBuilder sb = new StringBuilder("a");
+ for (int i = 0; i < 10; i++) {
+ sb.append("b");
+ }
+ analyzeString(sb.toString());
+
+ int limit = new Random().nextInt();
+ sb = new StringBuilder("a");
+ for (int i = 0; i < limit; i++) {
+ sb.append("b");
+ }
+ analyzeString(sb.toString());
+ }
+
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "((x|^-?\\d+$))*yz")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "xyz")
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(.*|.*yz)")
+ public void ifElseInLoopWithAppendAfterwards() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 20; i++) {
+ if (i % 2 == 0) {
+ sb.append("x");
+ } else {
+ sb.append(i + 1);
+ }
+ }
+ sb.append("yz");
+
+ analyzeString(sb.toString());
+ }
+
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "a(b)*")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab")
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*")
+ public void nestedLoops(int range) {
+ for (int i = 0; i < range; i++) {
+ StringBuilder sb = new StringBuilder("a");
+ for (int j = 0; j < range * range; j++) {
+ sb.append("b");
+ }
+ analyzeString(sb.toString());
+ }
+ }
+
+ @PartiallyConstant(n = 0, value = "((x|^-?\\d+$))*yz", levels = Level.TRUTH)
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "xyz")
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(.*|.*yz)")
+ public void stringBufferExample() {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < 20; i++) {
+ if (i % 2 == 0) {
+ sb.append("x");
+ } else {
+ sb.append(i + 1);
+ }
+ }
+ sb.append("yz");
+
+ analyzeString(sb.toString());
+ }
+
+ @PartiallyConstant(n = 0, value = "a(b)*", levels = Level.TRUTH)
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab")
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*")
+ public void whileTrueWithBreak() {
+ StringBuilder sb = new StringBuilder("a");
+ while (true) {
+ sb.append("b");
+ if (sb.length() > 100) {
+ break;
+ }
+ }
+ analyzeString(sb.toString());
+ }
+
+ @PartiallyConstant(n = 0, value = "a(b)*", levels = Level.TRUTH)
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab")
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*")
+ public void whileNonTrueWithBreak(int i) {
+ StringBuilder sb = new StringBuilder("a");
+ int j = 0;
+ while (j < i) {
+ sb.append("b");
+ if (sb.length() > 100) {
+ break;
+ }
+ j++;
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(iv1|iv2): ")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "(iv1|iv2): ")
+ // The real value is not fully resolved yet, since the string builder is used in a while loop,
+ // which leads to the string builder potentially carrying any value. This can be refined by
+ // recording pc specific states during data flow analysis.
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "((iv1|iv2): |.*)")
+ @PartiallyConstant(n = 1, levels = Level.TRUTH, value = "(iv1|iv2): ((great!)?)*(java.lang.Runtime)?")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 1, levels = Level.L1, soundness = SoundnessMode.LOW, value = "(iv1|iv2): great!")
+ @Dynamic(n = 1, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "(.*|.*.*)")
+ @Constant(n = 1, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "((iv1|iv2): great!|(iv1|iv2): great!java.lang.Runtime)")
+ @Dynamic(n = 1, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(.*|.*java.lang.Runtime)")
+ public void extensiveWithManyControlStructures(boolean cond) {
+ StringBuilder sb = new StringBuilder();
+ if (cond) {
+ sb.append("iv1");
+ } else {
+ sb.append("iv2");
+ }
+ System.out.println(sb);
+ sb.append(": ");
+
+ analyzeString(sb.toString());
+
+ Random random = new Random();
+ while (random.nextFloat() > 5.) {
+ if (random.nextInt() % 2 == 0) {
+ sb.append("great!");
+ }
+ }
+
+ if (sb.indexOf("great!") > -1) {
+ sb.append(getRuntimeClassName());
+ }
+
+ analyzeString(sb.toString());
+ }
+
+ // The bytecode produces an "if" within an "if" inside the first loop => two conditions
+ @Constant(n = 0, levels = Level.TRUTH, value = "abc((d)?)*")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "(abc|abcd)")
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*")
+ @Constant(n = 1, levels = Level.TRUTH, value = "")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "")
+ @Dynamic(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(|.*)")
+ @Dynamic(n = 2, levels = Level.TRUTH, value = "((.*)?)*")
+ @Failure(n = 2, levels = Level.L0)
+ @Constant(n = 2, levels = Level.L1, soundness = SoundnessMode.LOW, value = "")
+ @Constant(n = 2, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "(|java.lang.Runtime)")
+ @Dynamic(n = 2, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*")
+ public void breakContinueExamples(int value) {
+ StringBuilder sb1 = new StringBuilder("abc");
+ for (int i = 0; i < value; i++) {
+ if (i % 7 == 1) {
+ break;
+ } else if (i % 3 == 0) {
+ continue;
+ } else {
+ sb1.append("d");
+ }
+ }
+ analyzeString(sb1.toString());
+
+ StringBuilder sb2 = new StringBuilder("");
+ for (int i = 0; i < value; i++) {
+ if (i % 2 == 0) {
+ break;
+ }
+ sb2.append("some_value");
+ }
+ analyzeString(sb2.toString());
+
+ StringBuilder sb3 = new StringBuilder();
+ for (int i = 0; i < 10; i++) {
+ if (sb3.toString().equals("")) {
+ // The analysis currently does not detect, that this statement is executed at
+ // most / exactly once as it fully relies on the three-address code and does not
+ // infer any semantics of conditionals
+ sb3.append(getRuntimeClassName());
+ } else {
+ continue;
+ }
+ }
+ analyzeString(sb3.toString());
+ }
+
+ /**
+ * Some comprehensive example for experimental purposes taken from the JDK and slightly modified
+ */
+ @Constant(n = 0, levels = Level.TRUTH, value = "Hello: (java.lang.Runtime|java.lang.StringBuilder|StringBuilder)?")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW, value = "Hello: ")
+ @Constant(n = 0, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW,
+ value = "(Hello: |Hello: StringBuilder|Hello: java.lang.Runtime|Hello: java.lang.StringBuilder)")
+ @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*")
+ protected void setDebugFlags(String[] var1) {
+ for(int var2 = 0; var2 < var1.length; ++var2) {
+ String var3 = var1[var2];
+
+ int randomValue = new Random().nextInt();
+ StringBuilder sb = new StringBuilder("Hello: ");
+ if (randomValue % 2 == 0) {
+ sb.append(getRuntimeClassName());
+ } else if (randomValue % 3 == 0) {
+ sb.append(getStringBuilderClassName());
+ } else if (randomValue % 4 == 0) {
+ sb.append(getSimpleStringBuilderClassName());
+ }
+
+ try {
+ Field var4 = this.getClass().getField(var3 + "DebugFlag");
+ int var5 = var4.getModifiers();
+ if (Modifier.isPublic(var5) && !Modifier.isStatic(var5) &&
+ var4.getType() == Boolean.TYPE) {
+ var4.setBoolean(this, true);
+ }
+ } catch (IndexOutOfBoundsException var90) {
+ System.out.println("Should never happen!");
+ } catch (Exception var6) {
+ int i = 10;
+ i += new Random().nextInt();
+ System.out.println("Some severe error occurred!" + i);
+ } finally {
+ int i = 10;
+ i += new Random().nextInt();
+ if (i % 2 == 0) {
+ System.out.println("Ready to analyze now in any case!" + i);
+ }
+ }
+
+ analyzeString(sb.toString());
+ }
+ }
+
+ private String getRuntimeClassName() {
+ return "java.lang.Runtime";
+ }
+
+ private String getStringBuilderClassName() {
+ return "java.lang.StringBuilder";
+ }
+
+ private String getSimpleStringBuilderClassName() {
+ return "StringBuilder";
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Result.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Result.java
new file mode 100644
index 0000000000..91ccd9a6ab
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Result.java
@@ -0,0 +1,30 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+/**
+ * Tests compatibility of the results of the string analysis with e.g. being compiled to a regex string.
+ *
+ * @see SimpleStringOps
+ */
+public class Result {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "\\[B")
+ @Constant(n = 1, levels = Level.TRUTH, value = "\\[Ljava.lang.String;")
+ @Constant(n = 2, levels = Level.TRUTH, value = "\\[\\[Lsun.security.pkcs.SignerInfo;")
+ @Constant(n = 3, levels = Level.TRUTH, value = "US\\$")
+ @Constant(n = 4, levels = Level.TRUTH, value = "US\\\\")
+ public void regexCompilableTest() {
+ analyzeString("[B");
+ analyzeString("[Ljava.lang.String;");
+ analyzeString("[[Lsun.security.pkcs.SignerInfo;");
+ analyzeString("US$");
+ analyzeString("US\\");
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleControlStructures.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleControlStructures.java
new file mode 100644
index 0000000000..f4d2fd3e17
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleControlStructures.java
@@ -0,0 +1,266 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+import java.util.Random;
+
+/**
+ * Various tests that test compatibility of the data flow analysis with simple control structures like if-statements.
+ *
+ * @see SimpleStringOps
+ */
+public class SimpleControlStructures {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ @Dynamic(n = 0, levels = Level.TRUTH, value = "(^-?\\d+$|x)")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "x")
+ @Constant(n = 1, levels = Level.TRUTH, value = "(42-42|x)")
+ @Failure(n = 1, levels = Level.L0)
+ public void ifElseWithStringBuilderWithIntExpr() {
+ StringBuilder sb1 = new StringBuilder();
+ StringBuilder sb2 = new StringBuilder();
+ int i = new Random().nextInt();
+ if (i % 2 == 0) {
+ sb1.append("x");
+ sb2.append(42);
+ sb2.append(-42);
+ } else {
+ sb1.append(i + 1);
+ sb2.append("x");
+ }
+ analyzeString(sb1.toString());
+ analyzeString(sb2.toString());
+ }
+
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "(3.142.71828|^-?\\d*\\.{0,1}\\d+$2.71828)")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "3.142.71828")
+ public void ifElseWithStringBuilderWithFloatExpr() {
+ StringBuilder sb1 = new StringBuilder();
+ int i = new Random().nextInt();
+ if (i % 2 == 0) {
+ sb1.append(3.14);
+ } else {
+ sb1.append(new Random().nextFloat());
+ }
+ float e = (float) 2.71828;
+ sb1.append(e);
+ analyzeString(sb1.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(a|b)")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "(ab|ac)")
+ @Failure(n = 1, levels = Level.L0)
+ public void ifElseWithStringBuilder() {
+ StringBuilder sb1;
+ StringBuilder sb2 = new StringBuilder("a");
+
+ int i = new Random().nextInt();
+ if (i % 2 == 0) {
+ sb1 = new StringBuilder("a");
+ sb2.append("b");
+ } else {
+ sb1 = new StringBuilder("b");
+ sb2.append("c");
+ }
+ analyzeString(sb1.toString());
+ analyzeString(sb2.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(abcd|axyz)")
+ @Failure(n = 0, levels = Level.L0)
+ public void ifElseWithStringBuilderWithMultipleAppends() {
+ StringBuilder sb = new StringBuilder("a");
+ int i = new Random().nextInt();
+ if (i % 2 == 0) {
+ sb.append("b");
+ sb.append("c");
+ sb.append("d");
+ } else {
+ sb.append("x");
+ sb.append("y");
+ sb.append("z");
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(a|abcd|axyz)")
+ @Failure(n = 0, levels = Level.L0)
+ public void ifElseWithStringBuilderWithMultipleAppendsAndNonUsedElseIf() {
+ StringBuilder sb = new StringBuilder("a");
+ int i = new Random().nextInt();
+ if (i % 3 == 0) {
+ sb.append("b");
+ sb.append("c");
+ sb.append("d");
+ } else if (i % 2 == 0) {
+ System.out.println("something");
+ } else {
+ sb.append("x");
+ sb.append("y");
+ sb.append("z");
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab)")
+ @Failure(n = 0, levels = Level.L0)
+ public void ifWithoutElse() {
+ StringBuilder sb = new StringBuilder("a");
+ int i = new Random().nextInt();
+ if (i % 2 == 0) {
+ sb.append("b");
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.Runtime")
+ @Failure(n = 0, levels = Level.L0)
+ public void ifConditionAppendsToString(String className) {
+ StringBuilder sb = new StringBuilder();
+ if (sb.append("java.lang.Runtime").toString().equals(className)) {
+ System.out.println("Yep, got the correct class!");
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab|ac)")
+ @Failure(n = 0, levels = Level.L0)
+ public void switchRelevantAndIrrelevant(int value) {
+ StringBuilder sb = new StringBuilder("a");
+ switch (value) {
+ case 0:
+ sb.append("b");
+ break;
+ case 1:
+ sb.append("c");
+ break;
+ case 3:
+ break;
+ case 4:
+ break;
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab|ac|ad)")
+ @Failure(n = 0, levels = Level.L0)
+ public void switchRelevantAndIrrelevantWithRelevantDefault(int value) {
+ StringBuilder sb = new StringBuilder("a");
+ switch (value) {
+ case 0:
+ sb.append("b");
+ break;
+ case 1:
+ sb.append("c");
+ break;
+ case 2:
+ break;
+ case 3:
+ break;
+ default:
+ sb.append("d");
+ break;
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab|ac)")
+ @Failure(n = 0, levels = Level.L0)
+ public void switchRelevantAndIrrelevantWithIrrelevantDefault(int value) {
+ StringBuilder sb = new StringBuilder("a");
+ switch (value) {
+ case 0:
+ sb.append("b");
+ break;
+ case 1:
+ sb.append("c");
+ break;
+ case 2:
+ break;
+ case 3:
+ break;
+ default:
+ break;
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(ab|ac|ad)")
+ @Failure(n = 0, levels = Level.L0)
+ public void switchRelevantWithRelevantDefault(int value) {
+ StringBuilder sb = new StringBuilder("a");
+ switch (value) {
+ case 0:
+ sb.append("b");
+ break;
+ case 1:
+ sb.append("c");
+ break;
+ default:
+ sb.append("d");
+ break;
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab|ac|ad|af)")
+ @Failure(n = 0, levels = Level.L0)
+ public void switchNestedNoNestedDefault(int value, int value2) {
+ StringBuilder sb = new StringBuilder("a");
+ switch (value) {
+ case 0:
+ sb.append("b");
+ break;
+ case 1:
+ switch (value2) {
+ case 0:
+ sb.append("c");
+ break;
+ case 1:
+ sb.append("d");
+ break;
+ }
+ break;
+ default:
+ sb.append("f");
+ break;
+ }
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(ab|ac|ad|ae|af)")
+ @Failure(n = 0, levels = Level.L0)
+ public void switchNestedWithNestedDefault(int value, int value2) {
+ StringBuilder sb = new StringBuilder("a");
+ switch (value) {
+ case 0:
+ sb.append("b");
+ break;
+ case 1:
+ switch (value2) {
+ case 0:
+ sb.append("c");
+ break;
+ case 1:
+ sb.append("d");
+ break;
+ default:
+ sb.append("e");
+ break;
+ }
+ break;
+ default:
+ sb.append("f");
+ break;
+ }
+ analyzeString(sb.toString());
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringBuilderOps.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringBuilderOps.java
new file mode 100644
index 0000000000..2d10463538
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringBuilderOps.java
@@ -0,0 +1,194 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * Various tests that test compatibility with selected methods defined on string builders and string buffers, such as
+ * append, reset etc.
+ *
+ * @see SimpleStringOps
+ */
+public class SimpleStringBuilderOps {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+ public void analyzeString(StringBuilder sb) {}
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String")
+ @Failure(n = 0, levels = Level.L0)
+ public void multipleDirectAppends() {
+ StringBuilder sb = new StringBuilder("java");
+ sb.append(".").append("lang").append(".").append("String");
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "SomeOther")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "SomeOther")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, value = "(Some|SomeOther)")
+ public void stringValueOfWithStringBuilder() {
+ StringBuilder sb = new StringBuilder("Some");
+ sb.append("Other");
+ analyzeString(String.valueOf(sb));
+
+ analyzeString(sb);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "Some")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "Other")
+ @Failure(n = 1, levels = Level.L0)
+ public void stringBuilderBufferInitArguments() {
+ StringBuilder sb = new StringBuilder("Some");
+ analyzeString(sb.toString());
+
+ StringBuffer sb2 = new StringBuffer("Other");
+ analyzeString(sb2.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.StringBuilder")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "java.lang.StringBuilder")
+ @Failure(n = 1, levels = Level.L0)
+ public void simpleClearExamples() {
+ StringBuilder sb1 = new StringBuilder("init_value:");
+ sb1.setLength(0);
+ sb1.append("java.lang.StringBuilder");
+
+ StringBuilder sb2 = new StringBuilder("init_value:");
+ System.out.println(sb2.toString());
+ sb2 = new StringBuilder();
+ sb2.append("java.lang.StringBuilder");
+
+ analyzeString(sb1.toString());
+ analyzeString(sb2.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(Goodbye|init_value:Hello, world!Goodbye)")
+ @Failure(n = 0, levels = Level.L0)
+ public void advancedClearExampleWithSetLength(int value) {
+ StringBuilder sb = new StringBuilder("init_value:");
+ if (value < 10) {
+ sb.setLength(0);
+ } else {
+ sb.append("Hello, world!");
+ }
+ sb.append("Goodbye");
+ analyzeString(sb.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "replaced_value")
+ @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 })
+ @Constant(n = 1, levels = Level.TRUTH, value = "(...:Goodbye|init_value:Hello, world!Goodbye)")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW,
+ value = "init_value:Hello, world!Goodbye")
+ @PartiallyConstant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH,
+ value = "(.*Goodbye|init_value:Hello, world!Goodbye)")
+ public void replaceExamples(int value) {
+ StringBuilder sb1 = new StringBuilder("init_value");
+ sb1.replace(0, 5, "replaced_");
+ analyzeString(sb1.toString());
+
+ sb1 = new StringBuilder("init_value:");
+ if (value < 10) {
+ sb1.replace(0, value, "...");
+ } else {
+ sb1.append("Hello, world!");
+ }
+ sb1.append("Goodbye");
+ analyzeString(sb1.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "B.")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "java.langStringB.")
+ @Failure(n = 1, levels = Level.L0)
+ public void directAppendConcatsWith2ndStringBuilder() {
+ StringBuilder sb = new StringBuilder("java");
+ StringBuilder sb2 = new StringBuilder("B");
+ sb.append('.').append("lang");
+ sb2.append('.');
+ sb.append("String");
+ sb.append(sb2.toString());
+ analyzeString(sb2.toString());
+ analyzeString(sb.toString());
+ }
+
+ /**
+ * Checks if the value of a string builder that depends on the complex construction of a second one can be determined.
+ */
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.(Object|Runtime)")
+ @Failure(n = 0, levels = Level.L0)
+ public void complexSecondStringBuilderRead(String className) {
+ StringBuilder sbObj = new StringBuilder("Object");
+ StringBuilder sbRun = new StringBuilder("Runtime");
+
+ StringBuilder sb1 = new StringBuilder();
+ if (sb1.length() == 0) {
+ sb1.append(sbObj.toString());
+ } else {
+ sb1.append(sbRun.toString());
+ }
+
+ StringBuilder sb2 = new StringBuilder("java.lang.");
+ sb2.append(sb1.toString());
+ analyzeString(sb2.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(java.lang.Object|java.lang.Runtime)")
+ @Failure(n = 0, levels = Level.L0)
+ public void simpleSecondStringBuilderRead(String className) {
+ StringBuilder sbObj = new StringBuilder("Object");
+ StringBuilder sbRun = new StringBuilder("Runtime");
+
+ StringBuilder sb1 = new StringBuilder("java.lang.");
+ if (sb1.length() == 0) {
+ sb1.append(sbObj.toString());
+ } else {
+ sb1.append(sbRun.toString());
+ }
+
+ analyzeString(sb1.toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(Object|ObjectRuntime)")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "(Runtime|RuntimeObject)")
+ @Failure(n = 1, levels = Level.L0)
+ public void crissCrossExample(String className) {
+ StringBuilder sbObj = new StringBuilder("Object");
+ StringBuilder sbRun = new StringBuilder("Runtime");
+
+ if (className.length() == 0) {
+ sbRun.append(sbObj.toString());
+ } else {
+ sbObj.append(sbRun.toString());
+ }
+
+ analyzeString(sbObj.toString());
+ analyzeString(sbRun.toString());
+ }
+
+ @Invalid(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW)
+ @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "File Content:.*", soundness = SoundnessMode.HIGH)
+ @Failure(n = 0, levels = Level.L0)
+ public void withUnknownAppendSource(String filename) throws IOException {
+ StringBuilder sb = new StringBuilder("File Content:");
+ String data = new String(Files.readAllBytes(Paths.get(filename)));
+ sb.append(data);
+ analyzeString(sb.toString());
+ }
+
+ // IMPROVE Add the following tests
+ // - Passing string builders as call parameters should result in a failure (called method can modify string builder)
+ // - Support generic function calls, in particular with upper return type bounds that are non-object
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringOps.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringOps.java
new file mode 100644
index 0000000000..ce890f5686
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringOps.java
@@ -0,0 +1,242 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.*;
+
+/**
+ * All files in this package define various tests for the string analysis. The following things are to be considered
+ * when adding test cases:
+ *
+ *
+ * -
+ * In order to trigger the analysis for a particular string variable, call the analyzeString method with the
+ * variable to be analyzed. Multiple calls to the sink analyzeString within the same test method are allowed.
+ *
+ * -
+ * For a given sink call, the expected string and its constancy can be defined using one of these annotations:
+ *
+ * -
+ * {@link Invalid} The given string variable does not contain any string analyzable by the string
+ * analysis. Usually used as a fallback in low-soundness mode.
+ *
+ * -
+ * {@link Constant} The given string variable contains only constant strings and its set of possible
+ * values is thus enumerable within finite time.
+ *
+ * -
+ * {@link PartiallyConstant} The given string variable contains strings which have some constant part
+ * concatenated with some dynamic part. Its set of possible values is constrained but not enumerable
+ * within finite time.
+ *
+ * -
+ * {@link Dynamic} The given string variable contains strings which only consist of dynamic information.
+ * Its set of possible values may be constrained but is definitely not enumerable within finite time.
+ * Usually used as a fallback in high-soundness mode.
+ *
+ * -
+ * {@link Failure} Combines {@link Invalid} and {@link Dynamic} by generating the former for test runs in
+ * low-soundness mode and the latter for test runs in high-soundness mode.
+ *
+ *
+ *
+ * -
+ * For each test run configuration (different domain level, different soundness mode, different analysis level)
+ * exactly one such annotation should be defined for each test function. For every annotation, the following
+ * information should / can be given:
+ *
+ * - (Required)
n = ?
: The index of the sink call that this annotation is defined for.
+ * -
+ * (Required)
value = "?"
: The expected value (see below for format).
+ * Cannot be defined for {@link Invalid} annotations.
+ *
+ * -
+ * (Required)
levels = ?
: One or multiple of {@link Level} to allow restricting an annotation
+ * to certain string analysis level configurations. The value {@link Level#TRUTH } may be used to explicitly
+ * define the ground truth that all test run configurations will fall back to if no more specific annotation
+ * is found.
+ *
+ * -
+ * (Optional)
domains = ?
: One or multiple of {@link DomainLevel} to allow restricting an
+ * annotation to certain domain level configurations.
+ *
+ * -
+ * (Optional)
soundness = ?
: One or multiple of {@link SoundnessMode} to allow restricting an
+ * annotation to certain soundness mode configurations.
+ *
+ * -
+ * (Optional)
reason = "?"
: Some reasoning for the given annotation type and value. Not part of
+ * the test output.
+ *
+ *
+ *
+ * -
+ * Expected values for string variables should be given in a reduced regex format:
+ *
+ * - The asterisk symbol (*) is used to indicate that a string (or part of it) can occur >= 0 times.
+ * -
+ * The pipe symbol is used to indicate that a string (or part of it) consists of one of several options
+ * (but definitely one of these values).
+ *
+ * - Brackets ("(" and ")") are used for nesting and grouping string expressions.
+ * -
+ * The string "^-?\d+$" represents (positive and negative) integer numbers. This RegExp has been taken from
+ * www.freeformatter.com/java-regex-tester.html
+ * as of 2019-02-02.
+ *
+ * -
+ * The string "^-?\\d*\\.{0,1}\\d+$" represents (positive and negative) float and double numbers. This RegExp
+ * has been taken from
+ * www.freeformatter.com/java-regex-tester.html
+ * as of 2019-02-02.
+ *
+ *
+ *
+ *
+ *
+ * This file defines various tests related to simple operations on strings and presence of multiple def sites of such
+ * strings.
+ *
+ * @author Maximilian Rüsch
+ */
+public class SimpleStringOps {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String")
+ @Constant(n = 1, levels = Level.TRUTH, value = "java.lang.String")
+ public void constantStringReads() {
+ analyzeString("java.lang.String");
+
+ String className = "java.lang.String";
+ analyzeString(className);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "c")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "42.3")
+ @Failure(n = 1, levels = Level.L0)
+ @Constant(n = 2, levels = Level.TRUTH, value = "java.lang.Runtime")
+ @Failure(n = 2, levels = { Level.L0, Level.L1 })
+ public void valueOfTest() {
+ analyzeString(String.valueOf('c'));
+ analyzeString(String.valueOf((float) 42.3));
+ analyzeString(String.valueOf(getRuntimeClassName()));
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "java.lang.Object")
+ @Failure(n = 1, levels = Level.L0)
+ public void simpleStringConcat() {
+ String className1 = "java.lang.";
+ System.out.println(className1);
+ className1 += "String";
+ analyzeString(className1);
+
+ String className2 = "java.";
+ System.out.println(className2);
+ className2 += "lang.";
+ System.out.println(className2);
+ className2 += "Object";
+ analyzeString(className2);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "va.")
+ @Failure(n = 0, levels = Level.L0)
+ @Constant(n = 1, levels = Level.TRUTH, value = "va.lang.")
+ @Failure(n = 1, levels = Level.L0)
+ public void simpleSubstring() {
+ String someString = "java.lang.";
+ analyzeString(someString.substring(2, 5));
+ analyzeString(someString.substring(2));
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(java.lang.Runtime|java.lang.System)")
+ public void multipleConstantDefSites(boolean cond) {
+ String s;
+ if (cond) {
+ s = "java.lang.System";
+ } else {
+ s = "java.lang.Runtime";
+ }
+ analyzeString(s);
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "It is (great|not great)")
+ @Failure(n = 0, levels = Level.L0)
+ public void appendWithTwoDefSites(int i) {
+ String s;
+ if (i > 0) {
+ s = "great";
+ } else {
+ s = "not great";
+ }
+ analyzeString(new StringBuilder("It is ").append(s).toString());
+ }
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "(Some|SomeOther)")
+ @Constant(n = 0, levels = Level.L0, soundness = SoundnessMode.LOW, value = "Some")
+ @Dynamic(n = 0, levels = Level.L0, soundness = SoundnessMode.HIGH, value = "(.*|Some)")
+ @Constant(n = 1, levels = Level.TRUTH, value = "(Impostor|Some)")
+ @Constant(n = 2, levels = Level.TRUTH, value = "(SomeImpostor|SomeOther)")
+ @Failure(n = 2, levels = Level.L0)
+ public void ternaryOperators(boolean flag) {
+ String s1 = "Some";
+ String s2 = s1 + "Other";
+ String s3 = "Impostor";
+
+ analyzeString(flag ? s1 : s2);
+ analyzeString(flag ? s1 : s3);
+ analyzeString(flag ? s1 + s3 : s2);
+ }
+
+ /**
+ * A more comprehensive case where multiple definition sites have to be considered each with a different string
+ * generation mechanism
+ */
+ @Constant(n = 0, levels = Level.TRUTH, value = "(java.lang.Object|java.lang.Runtime|java.lang.System|java.lang.StringBuilder)")
+ @Constant(n = 0, levels = Level.L0, soundness = SoundnessMode.LOW, value = "java.lang.System")
+ @Dynamic(n = 0, levels = Level.L0, soundness = SoundnessMode.HIGH, value = "(.*|java.lang.System)")
+ @Constant(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW, value = "java.lang.System")
+ @Dynamic(n = 0, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "(.*|java.lang..*|java.lang.System)")
+ @Constant(n = 0, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "(java.lang.StringBuilder|java.lang.StringBuilder|java.lang.System)")
+ @Dynamic(n = 0, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(.*|java.lang.StringBuilder|java.lang.StringBuilder|java.lang.System)")
+ public void multipleDefSites(int value) {
+ String[] arr = new String[] { "java.lang.Object", getRuntimeClassName() };
+
+ String s;
+ switch (value) {
+ case 0:
+ s = arr[value];
+ break;
+ case 1:
+ s = arr[value];
+ break;
+ case 3:
+ s = "java.lang.System";
+ break;
+ case 4:
+ s = "java.lang." + getSimpleStringBuilderClassName();
+ break;
+ default:
+ s = getStringBuilderClassName();
+ }
+
+ analyzeString(s);
+ }
+
+ private String getRuntimeClassName() {
+ return "java.lang.Runtime";
+ }
+
+ private String getStringBuilderClassName() {
+ return "java.lang.StringBuilder";
+ }
+
+ private String getSimpleStringBuilderClassName() {
+ return "StringBuilder";
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SystemProperties.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SystemProperties.java
new file mode 100644
index 0000000000..a328619b4c
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SystemProperties.java
@@ -0,0 +1,27 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string;
+
+import org.opalj.fpcf.properties.string.Constant;
+import org.opalj.fpcf.properties.string.Failure;
+import org.opalj.fpcf.properties.string.Level;
+
+/**
+ * Tests the integration with the system properties FPCF property.
+ *
+ * @see SimpleStringOps
+ */
+public class SystemProperties {
+
+ /**
+ * Serves as the sink for string variables to be analyzed.
+ */
+ public void analyzeString(String s) {}
+
+ @Constant(n = 0, levels = Level.TRUTH, value = "some.test.value")
+ @Failure(n = 0, levels = Level.L0)
+ public void systemPropertiesIntegrationTest() {
+ System.setProperty("some.test.property", "some.test.value");
+ String s = System.getProperty("some.test.property");
+ analyzeString(s);
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/ParameterDependentStringFactory.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/ParameterDependentStringFactory.java
new file mode 100644
index 0000000000..198d514828
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/ParameterDependentStringFactory.java
@@ -0,0 +1,11 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string.tools;
+
+public class ParameterDependentStringFactory implements StringFactory {
+
+ @Override
+ public String getString(String parameter) {
+ return "Hello " + parameter;
+ }
+
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/SimpleStringFactory.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/SimpleStringFactory.java
new file mode 100644
index 0000000000..f8fe33378d
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/SimpleStringFactory.java
@@ -0,0 +1,11 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string.tools;
+
+public class SimpleStringFactory implements StringFactory {
+
+ @Override
+ public String getString(String parameter) {
+ return "Hello";
+ }
+
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringFactory.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringFactory.java
new file mode 100644
index 0000000000..e611cf693f
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringFactory.java
@@ -0,0 +1,9 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string.tools;
+
+public interface StringFactory {
+
+ String getString(String parameter);
+
+}
+
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringProvider.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringProvider.java
new file mode 100644
index 0000000000..a326374a31
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringProvider.java
@@ -0,0 +1,23 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.fixtures.string.tools;
+
+public class StringProvider {
+
+ /**
+ * Returns "[packageName].[className]".
+ */
+ public static String concat(String firstString, String secondString) {
+ return firstString + secondString;
+ }
+
+ /**
+ * Returns "[packageName].[className]".
+ */
+ public static String getFQClassNameWithStringBuilder(String packageName, String className) {
+ return (new StringBuilder()).append(packageName).append(".").append(className).toString();
+ }
+
+ public static String getSomeValue() {
+ return "someValue";
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constant.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constant.java
new file mode 100644
index 0000000000..72437d501b
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constant.java
@@ -0,0 +1,33 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import org.opalj.fpcf.properties.PropertyValidator;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@PropertyValidator(key = "StringConstancy", validator = ConstantStringMatcher.class)
+@Documented
+@Repeatable(Constants.class)
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD, ElementType.LOCAL_VARIABLE })
+public @interface Constant {
+
+ int n();
+
+ String reason() default "N/A";
+
+ /**
+ * A regexp like string that describes the element(s) that are expected.
+ */
+ String value();
+
+ Level[] levels();
+
+ DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 };
+
+ SoundnessMode[] soundness() default { SoundnessMode.LOW, SoundnessMode.HIGH };
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constants.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constants.java
new file mode 100644
index 0000000000..4be7497463
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constants.java
@@ -0,0 +1,16 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface Constants {
+
+ Constant[] value();
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/DomainLevel.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/DomainLevel.java
new file mode 100644
index 0000000000..0b6330375f
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/DomainLevel.java
@@ -0,0 +1,22 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+public enum DomainLevel {
+
+ L1("L1"),
+ L2("L2");
+
+ private final String value;
+
+ DomainLevel(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamic.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamic.java
new file mode 100644
index 0000000000..f6a422e9c6
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamic.java
@@ -0,0 +1,33 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import org.opalj.fpcf.properties.PropertyValidator;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@PropertyValidator(key = "StringConstancy", validator = DynamicStringMatcher.class)
+@Documented
+@Repeatable(Dynamics.class)
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface Dynamic {
+
+ int n();
+
+ String reason() default "N/A";
+
+ /**
+ * A regexp like string that describes the element(s) that are expected.
+ */
+ String value();
+
+ Level[] levels();
+
+ DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 };
+
+ SoundnessMode[] soundness() default { SoundnessMode.LOW, SoundnessMode.HIGH };
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamics.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamics.java
new file mode 100644
index 0000000000..ab0b34b717
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamics.java
@@ -0,0 +1,16 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface Dynamics {
+
+ Dynamic[] value();
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failure.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failure.java
new file mode 100644
index 0000000000..505c8d3a65
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failure.java
@@ -0,0 +1,25 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import java.lang.annotation.*;
+
+/**
+ * Note that this annotation will be rewritten into {@link Invalid} or {@link Dynamic} depending on the soundness mode.
+ *
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@Documented
+@Repeatable(Failures.class)
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface Failure {
+
+ int n();
+
+ Level[] levels();
+
+ String reason() default "N/A";
+
+ DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 };
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failures.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failures.java
new file mode 100644
index 0000000000..1a8c57bcbb
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failures.java
@@ -0,0 +1,16 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface Failures {
+
+ Failure[] value();
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalid.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalid.java
new file mode 100644
index 0000000000..d19f597681
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalid.java
@@ -0,0 +1,28 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import org.opalj.fpcf.properties.PropertyValidator;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@PropertyValidator(key = "StringConstancy", validator = InvalidStringMatcher.class)
+@Documented
+@Repeatable(Invalids.class)
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface Invalid {
+
+ int n();
+
+ String reason() default "N/A";
+
+ Level[] levels();
+
+ DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 };
+
+ SoundnessMode[] soundness() default { SoundnessMode.LOW, SoundnessMode.HIGH };
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalids.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalids.java
new file mode 100644
index 0000000000..a47be759ca
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalids.java
@@ -0,0 +1,16 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface Invalids {
+
+ Invalid[] value();
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Level.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Level.java
new file mode 100644
index 0000000000..cec1133a6f
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Level.java
@@ -0,0 +1,25 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+public enum Level {
+
+ TRUTH("TRUTH"),
+ L0("L0"),
+ L1("L1"),
+ L2("L2"),
+ L3("L3");
+
+ private final String value;
+
+ Level(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstant.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstant.java
new file mode 100644
index 0000000000..53f9178533
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstant.java
@@ -0,0 +1,33 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import org.opalj.fpcf.properties.PropertyValidator;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@PropertyValidator(key = "StringConstancy", validator = PartiallyConstantStringMatcher.class)
+@Documented
+@Repeatable(PartiallyConstants.class)
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface PartiallyConstant {
+
+ int n();
+
+ String reason() default "N/A";
+
+ /**
+ * A regexp like string that describes the element(s) that are expected.
+ */
+ String value();
+
+ Level[] levels();
+
+ DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 };
+
+ SoundnessMode[] soundness() default { SoundnessMode.LOW, SoundnessMode.HIGH };
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstants.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstants.java
new file mode 100644
index 0000000000..ca5f0233ff
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstants.java
@@ -0,0 +1,16 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+import java.lang.annotation.*;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.METHOD })
+public @interface PartiallyConstants {
+
+ PartiallyConstant[] value();
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/SoundnessMode.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/SoundnessMode.java
new file mode 100644
index 0000000000..8494678f99
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/SoundnessMode.java
@@ -0,0 +1,22 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj.fpcf.properties.string;
+
+/**
+ * @see org.opalj.fpcf.fixtures.string.SimpleStringOps
+ * @author Maximilian Rüsch
+ */
+public enum SoundnessMode {
+
+ LOW("LOW"),
+ HIGH("HIGH");
+
+ private final String value;
+
+ SoundnessMode(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PropertiesTest.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PropertiesTest.scala
index fef3b9b5d3..9ed2f1476d 100644
--- a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PropertiesTest.scala
+++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PropertiesTest.scala
@@ -361,33 +361,42 @@ abstract class PropertiesTest extends AnyFunSpec with Matchers {
}
def executeAnalyses(
- analysisRunners: Iterable[ComputationSpecification[FPCFAnalysis]]
+ analysisRunners: Iterable[ComputationSpecification[FPCFAnalysis]],
+ afterPhaseScheduling: (Project[URL], List[ComputationSpecification[FPCFAnalysis]]) => Unit = (_, _) => ()
): TestContext = {
try {
- val p = FixtureProject.recreate { piKeyUnidueId => piKeyUnidueId != PropertyStoreKey.uniqueId } // to ensure that this project is not "polluted"
- implicit val logContext: LogContext = p.logContext
+ val p = FixtureProject.recreate { piKeyUniqueId => piKeyUniqueId != PropertyStoreKey.uniqueId } // to ensure that this project is not "polluted"
+
init(p)
+ executeAnalysesForProject(p, analysisRunners, afterPhaseScheduling = afterPhaseScheduling(p, _))
+ } catch {
+ case t: Throwable =>
+ t.printStackTrace()
+ t.getSuppressed.foreach(e => e.printStackTrace())
+ throw t;
+ }
+ }
+
+ def executeAnalysesForProject(
+ project: Project[URL],
+ analysisRunners: Iterable[ComputationSpecification[FPCFAnalysis]],
+ afterPhaseScheduling: List[ComputationSpecification[FPCFAnalysis]] => Unit = _ => ()
+ ): TestContext = {
+ try {
+ implicit val logContext: LogContext = project.logContext
+
PropertyStore.updateDebug(true)
- p.getOrCreateProjectInformationKeyInitializationData(
+ project.getOrCreateProjectInformationKeyInitializationData(
PropertyStoreKey,
- (context: List[PropertyStoreContext[AnyRef]]) => {
- /*
- val ps = PKEParallelTasksPropertyStore.create(
- new RecordAllPropertyStoreTracer,
- context.iterator.map(_.asTuple).toMap
- )
- */
- val ps = PKESequentialPropertyStore(context: _*)
- ps
- }
+ (context: List[PropertyStoreContext[AnyRef]]) => PKESequentialPropertyStore(context: _*)
)
- val ps = p.get(PropertyStoreKey)
+ val ps = project.get(PropertyStoreKey)
- val (_, csas) = p.get(FPCFAnalysesManagerKey).runAll(analysisRunners)
- TestContext(p, ps, csas.collect { case (_, as) => as })
+ val (_, csas) = project.get(FPCFAnalysesManagerKey).runAll(analysisRunners, afterPhaseScheduling(_))
+ TestContext(project, ps, csas.collect { case (_, as) => as })
} catch {
case t: Throwable =>
t.printStackTrace()
diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/StringAnalysisTest.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/StringAnalysisTest.scala
new file mode 100644
index 0000000000..8c04193921
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/StringAnalysisTest.scala
@@ -0,0 +1,428 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package fpcf
+
+import java.net.URL
+import java.util
+
+import com.typesafe.config.Config
+import com.typesafe.config.ConfigValueFactory
+
+import org.opalj.ai.Domain
+import org.opalj.ai.domain.RecordDefUse
+import org.opalj.ai.domain.l1.DefaultDomainWithCFGAndDefUse
+import org.opalj.ai.domain.l2.DefaultPerformInvocationsDomainWithCFGAndDefUse
+import org.opalj.ai.fpcf.properties.AIDomainFactoryKey
+import org.opalj.br.Annotation
+import org.opalj.br.Annotations
+import org.opalj.br.ElementValue
+import org.opalj.br.ElementValuePair
+import org.opalj.br.ElementValuePairs
+import org.opalj.br.Method
+import org.opalj.br.ObjectType
+import org.opalj.br.StringValue
+import org.opalj.br.analyses.DeclaredMethodsKey
+import org.opalj.br.analyses.FieldAccessInformationKey
+import org.opalj.br.analyses.Project
+import org.opalj.br.fpcf.ContextProviderKey
+import org.opalj.br.fpcf.FPCFAnalysis
+import org.opalj.br.fpcf.PropertyStoreKey
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.fpcf.properties.string.Constant
+import org.opalj.fpcf.properties.string.Constants
+import org.opalj.fpcf.properties.string.DomainLevel
+import org.opalj.fpcf.properties.string.Dynamic
+import org.opalj.fpcf.properties.string.Dynamics
+import org.opalj.fpcf.properties.string.Failure
+import org.opalj.fpcf.properties.string.Failures
+import org.opalj.fpcf.properties.string.Invalid
+import org.opalj.fpcf.properties.string.Invalids
+import org.opalj.fpcf.properties.string.Level
+import org.opalj.fpcf.properties.string.PartiallyConstant
+import org.opalj.fpcf.properties.string.PartiallyConstants
+import org.opalj.fpcf.properties.string.SoundnessMode
+import org.opalj.log.OPALLogger
+import org.opalj.tac.EagerDetachedTACAIKey
+import org.opalj.tac.PV
+import org.opalj.tac.TACMethodParameter
+import org.opalj.tac.TACode
+import org.opalj.tac.V
+import org.opalj.tac.VirtualMethodCall
+import org.opalj.tac.cg.CallGraphKey
+import org.opalj.tac.cg.RTACallGraphKey
+import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis
+import org.opalj.tac.fpcf.analyses.string.StringAnalysis
+import org.opalj.tac.fpcf.analyses.string.StringAnalysisConfig
+import org.opalj.tac.fpcf.analyses.string.VariableContext
+import org.opalj.tac.fpcf.analyses.string.flowanalysis.MethodStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l0.LazyL0StringAnalysis
+import org.opalj.tac.fpcf.analyses.string.l1.LazyL1StringAnalysis
+import org.opalj.tac.fpcf.analyses.string.l2.LazyL2StringAnalysis
+import org.opalj.tac.fpcf.analyses.string.l3.LazyL3StringAnalysis
+import org.opalj.tac.fpcf.analyses.systemproperties.TriggeredSystemPropertiesAnalysisScheduler
+
+// IMPROVE the test runner structure is far from optimal and could be reduced down to a simple test matrix. This however
+// would require generating the wrapping "describes" and tag the tests using e.g. "asTagged" to be able to filter them
+// during runs.
+sealed abstract class StringAnalysisTest extends PropertiesTest {
+
+ // The name of the method from which to extract PUVars to analyze.
+ val nameTestMethod: String = "analyzeString"
+
+ final val callGraphKey: CallGraphKey = RTACallGraphKey
+
+ def level: Level
+ def analyses: Iterable[ComputationSpecification[FPCFAnalysis]]
+ def domainLevel: DomainLevel
+ def soundnessMode: SoundnessMode
+
+ override def createConfig(): Config = {
+ val highSoundness = soundnessMode match {
+ case SoundnessMode.HIGH => true
+ case SoundnessMode.LOW => false
+ }
+
+ super.createConfig()
+ .withValue(StringAnalysisConfig.HighSoundnessConfigKey, ConfigValueFactory.fromAnyRef(highSoundness))
+ .withValue(StringAnalysis.DepthThresholdConfigKey, ConfigValueFactory.fromAnyRef(10))
+ .withValue(
+ MethodStringFlowAnalysis.ExcludedPackagesConfigKey,
+ ConfigValueFactory.fromIterable(new util.ArrayList[String]())
+ )
+ }
+
+ override def fixtureProjectPackage: List[String] = List("org/opalj/fpcf/fixtures/string")
+
+ override def init(p: Project[URL]): Unit = {
+ val domain = domainLevel match {
+ case DomainLevel.L1 => classOf[DefaultDomainWithCFGAndDefUse[_]]
+ case DomainLevel.L2 => classOf[DefaultPerformInvocationsDomainWithCFGAndDefUse[_]]
+ case _ => throw new IllegalArgumentException(s"Invalid domain level for test definition: $domainLevel")
+ }
+ p.updateProjectInformationKeyInitializationData(AIDomainFactoryKey) {
+ case None => Set(domain)
+ case Some(requirements) => requirements + domain
+ }
+ p.updateProjectInformationKeyInitializationData(EagerDetachedTACAIKey) {
+ case None => m => domain.getConstructors.head.newInstance(p, m).asInstanceOf[Domain with RecordDefUse]
+ case Some(_) => m => domain.getConstructors.head.newInstance(p, m).asInstanceOf[Domain with RecordDefUse]
+ }
+
+ val typeIterator = callGraphKey.getTypeIterator(p)
+ p.updateProjectInformationKeyInitializationData(ContextProviderKey) { _ => typeIterator }
+ }
+
+ describe(s"using level=$level, domainLevel=$domainLevel, soundness=$soundnessMode") {
+ describe(s"the string analysis is started") {
+ var entities = Iterable.empty[(VariableContext, Method)]
+ val project = FixtureProject.recreate { piKeyUniqueId => piKeyUniqueId != PropertyStoreKey.uniqueId }
+ init(project)
+
+ val as = executeAnalysesForProject(
+ project,
+ callGraphKey.allCallGraphAnalyses(project) ++ analyses,
+ currentPhaseAnalyses => {
+ if (currentPhaseAnalyses.exists(_.derives.exists(_.pk == StringConstancyProperty))) {
+ val ps = project.get(PropertyStoreKey)
+ entities = determineEntitiesToAnalyze(project)
+ entities.foreach(entity => ps.force(entity._1, StringConstancyProperty.key))
+ }
+ }
+ )
+
+ as.propertyStore.waitOnPhaseCompletion()
+ as.propertyStore.shutdown()
+
+ validateProperties(as, determineEAS(entities, project), Set("StringConstancy"))
+ }
+ }
+
+ def determineEntitiesToAnalyze(project: Project[URL]): Iterable[(VariableContext, Method)] = {
+ val tacProvider = project.get(EagerDetachedTACAIKey)
+ val declaredMethods = project.get(DeclaredMethodsKey)
+ val contextProvider = project.get(ContextProviderKey)
+ project.allMethods
+ .filter(_.runtimeInvisibleAnnotations.nonEmpty)
+ .foldLeft(Seq.empty[(VariableContext, Method)]) { (entities, m) =>
+ entities ++ extractPUVars(tacProvider(m)).map(e =>
+ (VariableContext(e._1, e._2, contextProvider.newContext(declaredMethods(m))), m)
+ )
+ }
+ }
+
+ /**
+ * Extracts [[org.opalj.br.PUVar]]s from a set of statements. The locations of the [[org.opalj.br.PUVar]]s are
+ * identified by the argument to the very first call to [[nameTestMethod]].
+ *
+ * @return Returns the arguments of the [[nameTestMethod]] as a PUVars list in the order in which they occurred.
+ */
+ def extractPUVars(tac: TACode[TACMethodParameter, V]): List[(Int, PV)] = {
+ tac.cfg.code.instructions.filter {
+ case VirtualMethodCall(_, _, _, name, _, _, _) => name == nameTestMethod
+ case _ => false
+ }.map { call => (call.pc, call.asVirtualMethodCall.params.head.asVar.toPersistentForm(tac.stmts)) }.toList
+ }
+
+ def determineEAS(
+ entities: Iterable[(VariableContext, Method)],
+ project: Project[URL]
+ ): Iterable[(VariableContext, String => String, List[Annotation])] = {
+ val m2e = entities.groupBy(_._2).iterator.map(e =>
+ e._1 -> (e._1, e._2.map(k => k._1))
+ ).toMap
+
+ var detectedMissingAnnotations = false
+ // As entity, we need not the method but a tuple (PUVar, Method), thus this transformation
+ val eas = methodsWithAnnotations(project).filter(am => m2e.contains(am._1)).flatMap { am =>
+ val annotationsByIndex = getCheckableAnnotationsByIndex(project, am._3)
+ m2e(am._1)._2.zipWithIndex.map {
+ case (vc, index) =>
+ // Ensure every test that is annotated for at least one configuration is annotated for all
+ // configurations so we do not miss tests when adding new levels.
+ if (annotationsByIndex(index).isEmpty) {
+ OPALLogger.error(
+ "string analysis test setup",
+ s"Could not find annotations to check for #$index of ${am._1.toJava.substring(24)}"
+ )(project.logContext)
+ detectedMissingAnnotations = true
+ }
+
+ Tuple3(
+ vc,
+ { s: String => s"${am._2(s)} (#$index)" },
+ annotationsByIndex(index).toList
+ )
+ }
+ }
+
+ if (detectedMissingAnnotations) {
+ throw new IllegalStateException("Detected missing tests for this configuration!")
+ }
+
+ eas
+ }
+
+ private def getCheckableAnnotationsByIndex(project: Project[URL], as: Annotations): Map[Int, Annotations] = {
+ def mapFailure(failureEvp: ElementValuePairs): Annotation = {
+ if (soundnessMode == SoundnessMode.HIGH)
+ new Annotation(
+ ObjectType(classOf[Dynamic].getName.replace(".", "/")),
+ failureEvp.appended(ElementValuePair("value", StringValue(".*")))
+ )
+ else
+ new Annotation(
+ ObjectType(classOf[Invalid].getName.replace(".", "/")),
+ failureEvp
+ )
+ }
+
+ as.flatMap {
+ case a @ Annotation(annotationType, evp)
+ if annotationType.toJavaClass == classOf[Constant]
+ || annotationType.toJavaClass == classOf[PartiallyConstant]
+ || annotationType.toJavaClass == classOf[Dynamic]
+ || annotationType.toJavaClass == classOf[Invalid] =>
+ Seq((evp.head.value.asIntValue.value, a))
+
+ case Annotation(annotationType, evp) if annotationType.toJavaClass == classOf[Failure] =>
+ Seq((evp.head.value.asIntValue.value, mapFailure(evp)))
+
+ case Annotation(annotationType, evp)
+ if annotationType.toJavaClass == classOf[Constants]
+ || annotationType.toJavaClass == classOf[PartiallyConstants]
+ || annotationType.toJavaClass == classOf[Dynamics]
+ || annotationType.toJavaClass == classOf[Invalids] =>
+ evp.head.value.asArrayValue.values.toSeq.map { av =>
+ val annotation = av.asAnnotationValue.annotation
+ (annotation.elementValuePairs.head.value.asIntValue.value, annotation)
+ }
+
+ case Annotation(annotationType, evp) if annotationType.toJavaClass == classOf[Failures] =>
+ evp.head.value.asArrayValue.values.toSeq.map { av =>
+ val annotation = av.asAnnotationValue.annotation
+ (annotation.elementValuePairs.head.value.asIntValue.value, mapFailure(annotation.elementValuePairs))
+ }
+
+ case _ =>
+ Seq.empty
+ }.groupBy(_._1).map { kv =>
+ val annotations = kv._2.map(_._2)
+ .filter(fulfillsDomainLevel(project, _, domainLevel))
+ .filter(fulfillsSoundness(project, _, soundnessMode))
+
+ val matchingCurrentLevel = annotations.filter(fulfillsLevel(project, _, level))
+ if (matchingCurrentLevel.isEmpty) {
+ (kv._1, annotations.filter(fulfillsLevel(project, _, Level.TRUTH)))
+ } else {
+ (kv._1, matchingCurrentLevel)
+ }
+ }
+ }
+
+ private def fulfillsLevel(p: Project[URL], a: Annotation, l: Level): Boolean = {
+ getValue(p, a, "levels").asArrayValue.values.exists(v => Level.valueOf(v.asEnumValue.constName) == l)
+ }
+
+ private def fulfillsDomainLevel(p: Project[URL], a: Annotation, dl: DomainLevel): Boolean = {
+ getValue(p, a, "domains").asArrayValue.values.exists(v => DomainLevel.valueOf(v.asEnumValue.constName) == dl)
+ }
+
+ private def fulfillsSoundness(p: Project[URL], a: Annotation, soundness: SoundnessMode): Boolean = {
+ getValue(p, a, "soundness").asArrayValue.values.exists { v =>
+ SoundnessMode.valueOf(v.asEnumValue.constName) == soundness
+ }
+ }
+
+ private def getValue(p: Project[URL], a: Annotation, name: String): ElementValue = {
+ a.elementValuePairs.collectFirst {
+ case ElementValuePair(`name`, value) => value
+ }.orElse {
+ // get default value ...
+ p.classFile(a.annotationType.asObjectType).get.findMethod(name).head.annotationDefault
+ }.get
+ }
+}
+
+/**
+ * Tests whether the [[org.opalj.tac.fpcf.analyses.string.l0.LazyL0StringAnalysis]] works correctly with
+ * respect to some well-defined tests.
+ *
+ * @author Maximilian Rüsch
+ */
+sealed abstract class L0StringAnalysisTest extends StringAnalysisTest {
+
+ override final def level = Level.L0
+
+ override final def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] =
+ LazyL0StringAnalysis.allRequiredAnalyses
+}
+
+class L0StringAnalysisWithL1DefaultDomainTest extends L0StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L1
+ override def soundnessMode: SoundnessMode = SoundnessMode.LOW
+}
+
+class L0StringAnalysisWithL2DefaultDomainTest extends L0StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L2
+ override def soundnessMode: SoundnessMode = SoundnessMode.LOW
+}
+
+class HighSoundnessL0StringAnalysisWithL2DefaultDomainTest extends L0StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L2
+ override def soundnessMode: SoundnessMode = SoundnessMode.HIGH
+}
+
+/**
+ * Tests whether the [[org.opalj.tac.fpcf.analyses.string.l1.LazyL1StringAnalysis]] works correctly with
+ * respect to some well-defined tests.
+ *
+ * @author Maximilian Rüsch
+ */
+sealed abstract class L1StringAnalysisTest extends StringAnalysisTest {
+
+ override final def level = Level.L1
+
+ override final def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] = {
+ LazyL1StringAnalysis.allRequiredAnalyses :+
+ TriggeredSystemPropertiesAnalysisScheduler
+ }
+}
+
+class L1StringAnalysisWithL1DefaultDomainTest extends L1StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L1
+ override def soundnessMode: SoundnessMode = SoundnessMode.LOW
+}
+
+class L1StringAnalysisWithL2DefaultDomainTest extends L1StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L2
+ override def soundnessMode: SoundnessMode = SoundnessMode.LOW
+}
+
+class HighSoundnessL1StringAnalysisWithL2DefaultDomainTest extends L1StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L2
+ override def soundnessMode: SoundnessMode = SoundnessMode.HIGH
+}
+
+/**
+ * Tests whether the [[org.opalj.tac.fpcf.analyses.string.l2.LazyL2StringAnalysis]] works correctly with
+ * respect to some well-defined tests.
+ *
+ * @author Maximilian Rüsch
+ */
+sealed abstract class L2StringAnalysisTest extends StringAnalysisTest {
+
+ override def level = Level.L2
+
+ override final def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] = {
+ LazyL2StringAnalysis.allRequiredAnalyses :+
+ TriggeredSystemPropertiesAnalysisScheduler
+ }
+}
+
+class L2StringAnalysisWithL1DefaultDomainTest extends L2StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L1
+ override def soundnessMode: SoundnessMode = SoundnessMode.LOW
+}
+
+class L2StringAnalysisWithL2DefaultDomainTest extends L2StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L2
+ override def soundnessMode: SoundnessMode = SoundnessMode.LOW
+}
+
+class HighSoundnessL2StringAnalysisWithL2DefaultDomainTest extends L2StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L2
+ override def soundnessMode: SoundnessMode = SoundnessMode.HIGH
+}
+
+/**
+ * Tests whether the [[org.opalj.tac.fpcf.analyses.string.l3.LazyL3StringAnalysis]] works correctly with
+ * respect to some well-defined tests.
+ *
+ * @author Maximilian Rüsch
+ */
+sealed abstract class L3StringAnalysisTest extends StringAnalysisTest {
+
+ override def level = Level.L3
+
+ override final def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] = {
+ LazyL3StringAnalysis.allRequiredAnalyses :+
+ EagerFieldAccessInformationAnalysis :+
+ TriggeredSystemPropertiesAnalysisScheduler
+ }
+
+ override def init(p: Project[URL]): Unit = {
+ super.init(p)
+
+ p.updateProjectInformationKeyInitializationData(FieldAccessInformationKey) {
+ case None => Seq(EagerFieldAccessInformationAnalysis)
+ case Some(requirements) => requirements :+ EagerFieldAccessInformationAnalysis
+ }
+ }
+}
+
+class L3StringAnalysisWithL1DefaultDomainTest extends L3StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L1
+ override def soundnessMode: SoundnessMode = SoundnessMode.LOW
+}
+
+class L3StringAnalysisWithL2DefaultDomainTest extends L3StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L2
+ override def soundnessMode: SoundnessMode = SoundnessMode.LOW
+}
+
+class HighSoundnessL3StringAnalysisWithL2DefaultDomainTest extends L3StringAnalysisTest {
+
+ override def domainLevel: DomainLevel = DomainLevel.L2
+ override def soundnessMode: SoundnessMode = SoundnessMode.HIGH
+}
diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string/StringMatcher.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string/StringMatcher.scala
new file mode 100644
index 0000000000..c175662979
--- /dev/null
+++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string/StringMatcher.scala
@@ -0,0 +1,85 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package fpcf
+package properties
+package string
+
+import java.util.regex.Pattern
+import scala.util.Try
+
+import org.opalj.br.AnnotationLike
+import org.opalj.br.ObjectType
+import org.opalj.br.analyses.Project
+import org.opalj.br.fpcf.properties.string.StringConstancyLevel
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+
+/**
+ * @see [[StringAnalysisTest]]
+ * @see [[org.opalj.fpcf.fixtures.string.SimpleStringOps]]
+ * @author Maximilian Rüsch
+ */
+sealed trait StringMatcher extends AbstractPropertyMatcher {
+
+ protected def getActualValues: Property => Option[(String, String)] = {
+ case prop: StringConstancyProperty =>
+ val tree = prop.tree.simplify.sorted
+ if (tree.isInvalid) {
+ None
+ } else {
+ Some((tree.constancyLevel.toString.toLowerCase, tree.toRegex))
+ }
+ case p => throw new IllegalArgumentException(s"Tried to extract values from non string property: $p")
+ }
+}
+
+sealed abstract class ConstancyStringMatcher(val constancyLevel: StringConstancyLevel) extends StringMatcher {
+
+ override def validateProperty(
+ p: Project[_],
+ as: Set[ObjectType],
+ entity: Any,
+ a: AnnotationLike,
+ properties: Iterable[Property]
+ ): Option[String] = {
+ val expectedConstancy = constancyLevel.toString.toLowerCase
+ val expectedStrings = a.elementValuePairs.find(_.name == "value").get.value.asStringValue.value
+ val actualValuesOpt = getActualValues(properties.head)
+ if (actualValuesOpt.isEmpty) {
+ Some(s"Level: $expectedConstancy, Strings: $expectedStrings")
+ } else {
+ val (actLevel, actString) = actualValuesOpt.get
+ if (expectedConstancy != actLevel || expectedStrings != actString) {
+ Some(s"Level: $expectedConstancy, Strings: $expectedStrings")
+ } else {
+ val patternTry = Try(Pattern.compile(actString))
+ if (patternTry.isFailure) {
+ Some(s"Same string, but it does not compile to a regex: ${patternTry.failed.get}")
+ } else {
+ None
+ }
+ }
+ }
+ }
+}
+
+class ConstantStringMatcher extends ConstancyStringMatcher(StringConstancyLevel.Constant)
+class PartiallyConstantStringMatcher extends ConstancyStringMatcher(StringConstancyLevel.PartiallyConstant)
+class DynamicStringMatcher extends ConstancyStringMatcher(StringConstancyLevel.Dynamic)
+
+class InvalidStringMatcher extends StringMatcher {
+
+ override def validateProperty(
+ p: Project[_],
+ as: Set[ObjectType],
+ entity: Any,
+ a: AnnotationLike,
+ properties: Iterable[Property]
+ ): Option[String] = {
+ val actualValuesOpt = getActualValues(properties.head)
+ if (actualValuesOpt.isDefined) {
+ Some(s"Invalid flow - No strings determined!")
+ } else {
+ None
+ }
+ }
+}
diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/SystemProperties.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/SystemProperties.scala
index d6a0db5bd9..d8ff07860b 100644
--- a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/SystemProperties.scala
+++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/SystemProperties.scala
@@ -4,6 +4,7 @@ package br
package fpcf
package properties
+import org.opalj.br.fpcf.properties.string.StringTreeNode
import org.opalj.fpcf.Entity
import org.opalj.fpcf.FallbackReason
import org.opalj.fpcf.Property
@@ -13,20 +14,35 @@ import org.opalj.fpcf.PropertyMetaInformation
import org.opalj.fpcf.PropertyStore
/**
- * TODO Documentation
+ * Holds the possible values that a [[java.util.Properties]] can take on, e.g. by analyzing the parameters given to
+ * calls like [[java.util.Properties.setProperty]] that are found in reachable methods.
+ *
+ * Currently, values are not distinguished by the keys they are set for since the key parameters may also take on any
+ * value conforming to their string tree, which can be infinitely many (see [[StringTreeNode]]).
+ *
+ * All existing analyses do not distinguish between the system-wide properties (set through [[System.setProperty]] or
+ * similar) and all other [[java.util.Properties]] instances.
*
- * @author Florian Kuebler
+ * @author Maximilian Rüsch
*/
sealed trait SystemPropertiesPropertyMetaInformation extends PropertyMetaInformation {
+
type Self = SystemProperties
}
-class SystemProperties(val properties: Map[String, Set[String]])
+case class SystemProperties(values: Set[StringTreeNode])
extends Property with SystemPropertiesPropertyMetaInformation {
+
+ def mergeWith(other: SystemProperties): SystemProperties = {
+ if (values == other.values) this
+ else SystemProperties(values ++ other.values)
+ }
+
final def key: PropertyKey[SystemProperties] = SystemProperties.key
}
object SystemProperties extends SystemPropertiesPropertyMetaInformation {
+
final val Name = "opalj.SystemProperties"
final val key: PropertyKey[SystemProperties] = {
@@ -35,7 +51,7 @@ object SystemProperties extends SystemPropertiesPropertyMetaInformation {
(_: PropertyStore, reason: FallbackReason, _: Entity) =>
reason match {
case PropertyIsNotDerivedByPreviouslyExecutedAnalysis =>
- new SystemProperties(Map.empty)
+ new SystemProperties(Set.empty)
case _ =>
throw new IllegalStateException(s"analysis required for property: $Name")
}
diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyLevel.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyLevel.scala
new file mode 100644
index 0000000000..984d83bacf
--- /dev/null
+++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyLevel.scala
@@ -0,0 +1,84 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package br
+package fpcf
+package properties
+package string
+
+/**
+ * Documents the string constancy that a string tree consisting of [[StringTreeNode]] has, meaning a summary whether the
+ * string tree in question is invalid, has at most constant values, has at most constant values concatenated with
+ * dynamic values or also contains un-concatenated dynamic values. The companion object also defines useful combination
+ * functions for instances of this trait.
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait StringConstancyLevel
+
+object StringConstancyLevel {
+
+ /**
+ * Indicates that a string has no value at a given read operation.
+ */
+ case object Invalid extends StringConstancyLevel
+
+ /**
+ * Indicates that a string has a constant value at a given read operation.
+ */
+ case object Constant extends StringConstancyLevel
+
+ /**
+ * Indicates that a string is partially constant (has a constant and a dynamic part) at some read operation.
+ */
+ case object PartiallyConstant extends StringConstancyLevel
+
+ /**
+ * Indicates that a string at some read operations has an unpredictable value.
+ */
+ case object Dynamic extends StringConstancyLevel
+
+ /**
+ * The more general StringConstancyLevel of the two given levels, i.e. the one that allows more possible
+ * values at the given read operation.
+ *
+ * @param level1 The first level.
+ * @param level2 The second level.
+ * @return Returns the more general level of both given inputs.
+ */
+ def meet(level1: StringConstancyLevel, level2: StringConstancyLevel): StringConstancyLevel = {
+ if (level1 == Dynamic || level2 == Dynamic) {
+ Dynamic
+ } else if (level1 == PartiallyConstant || level2 == PartiallyConstant) {
+ PartiallyConstant
+ } else if (level1 == Constant || level2 == Constant) {
+ Constant
+ } else {
+ Invalid
+ }
+ }
+
+ /**
+ * Returns the StringConstancyLevel of a concatenation of two values.
+ *
+ * - Constant + Constant = Constant
+ * - Dynamic + Dynamic = Dynamic
+ * - Constant + Dynamic = PartiallyConstant
+ * - PartiallyConstant + {Dynamic, Constant} = PartiallyConstant
+ *
+ *
+ * @param level1 The first level.
+ * @param level2 The second level.
+ * @return Returns the level for a concatenation.
+ */
+ def determineForConcat(level1: StringConstancyLevel, level2: StringConstancyLevel): StringConstancyLevel = {
+ if (level1 == Invalid || level2 == Invalid) {
+ Invalid
+ } else if (level1 == PartiallyConstant || level2 == PartiallyConstant) {
+ PartiallyConstant
+ } else if ((level1 == Constant && level2 == Dynamic) || (level1 == Dynamic && level2 == Constant)) {
+ PartiallyConstant
+ } else {
+ level1
+ }
+ }
+}
diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyProperty.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyProperty.scala
new file mode 100644
index 0000000000..245b420310
--- /dev/null
+++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyProperty.scala
@@ -0,0 +1,77 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package br
+package fpcf
+package properties
+package string
+
+import org.opalj.fpcf.Entity
+import org.opalj.fpcf.FallbackReason
+import org.opalj.fpcf.Property
+import org.opalj.fpcf.PropertyIsNotDerivedByPreviouslyExecutedAnalysis
+import org.opalj.fpcf.PropertyKey
+import org.opalj.fpcf.PropertyMetaInformation
+import org.opalj.fpcf.PropertyStore
+
+/**
+ * Wrapper property around [[StringTreeNode]] to allow it to be stored in the [[PropertyStore]].
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait StringConstancyPropertyMetaInformation extends PropertyMetaInformation {
+ final type Self = StringConstancyProperty
+}
+
+class StringConstancyProperty(
+ val tree: StringTreeNode
+) extends Property with StringConstancyPropertyMetaInformation {
+
+ final def key: PropertyKey[StringConstancyProperty] = StringConstancyProperty.key
+
+ override def toString: String = {
+ val level = tree.constancyLevel
+ val strings = if (level == StringConstancyLevel.Invalid) {
+ "No possible strings - Invalid Flow"
+ } else tree.sorted.toRegex
+
+ s"Level: ${level.toString.toLowerCase}, Possible Strings: $strings"
+ }
+
+ override def hashCode(): Int = tree.hashCode()
+
+ override def equals(o: Any): Boolean = o match {
+ case scp: StringConstancyProperty => tree.equals(scp.tree)
+ case _ => false
+ }
+}
+
+object StringConstancyProperty extends Property with StringConstancyPropertyMetaInformation {
+
+ final val PropertyKeyName = "opalj.StringConstancy"
+
+ final val key: PropertyKey[StringConstancyProperty] = {
+ PropertyKey.create(
+ PropertyKeyName,
+ (_: PropertyStore, reason: FallbackReason, _: Entity) => {
+ reason match {
+ case PropertyIsNotDerivedByPreviouslyExecutedAnalysis =>
+ lb
+ case _ =>
+ throw new IllegalStateException(s"Analysis required for property: $PropertyKeyName")
+ }
+ }
+ )
+ }
+
+ def apply(tree: StringTreeNode): StringConstancyProperty = new StringConstancyProperty(tree)
+
+ /**
+ * @return The lower bound from a lattice-point of view.
+ */
+ def lb: StringConstancyProperty = StringConstancyProperty(StringTreeNode.lb)
+
+ /**
+ * @return The upper bound from a lattice-point of view.
+ */
+ def ub: StringConstancyProperty = StringConstancyProperty(StringTreeNode.ub)
+}
diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringTreeNode.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringTreeNode.scala
new file mode 100644
index 0000000000..2467f04cc3
--- /dev/null
+++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringTreeNode.scala
@@ -0,0 +1,490 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package br
+package fpcf
+package properties
+package string
+
+import scala.util.Try
+import scala.util.matching.Regex
+
+/**
+ * A single node that can be nested to create string trees that represent a set of possible string values. Its canonical
+ * reduction is a regex of all possible strings.
+ *
+ * @note This trait and all its implementations should be kept immutable to allow certain values to be cached.
+ * @see [[CachedHashCode]] [[CachedSimplifyNode]]
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait StringTreeNode {
+
+ val children: Seq[StringTreeNode]
+
+ /**
+ * The depth of the string tree measured by the count of nodes on the longest path from the root to a leaf.
+ */
+ lazy val depth: Int = children.map(_.depth).maxOption.getOrElse(0) + 1
+
+ /**
+ * Replaces string tree nodes at the target depth if they have children. In case a [[SimpleStringTreeNode]] is given
+ * as a second parameter, this effectively limits the string tree to the given target depth.
+ *
+ * @param targetDepth The depth at which nodes should be replaced if they have children.
+ * @param replacement The replacement to set for nodes at the target depth if they have children.
+ * @return The modified tree if the target depth is smaller than the current depth or the same instance if it is not.
+ */
+ final def replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = {
+ if (targetDepth >= depth)
+ this
+ else
+ _replaceAtDepth(targetDepth, replacement)
+ }
+ protected def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode
+
+ /**
+ * @return The string tree sorted with a stable ordering over its canonical reduction.
+ */
+ def sorted: StringTreeNode
+
+ private var _regex: Option[String] = None
+
+ /**
+ * @return The canonical reduction of the string tree, i.e. a regex representing the same set of string values as
+ * the tree itself.
+ */
+ final def toRegex: String = {
+ if (_regex.isEmpty) {
+ _regex = Some(_toRegex)
+ }
+
+ _regex.get
+ }
+ protected def _toRegex: String
+
+ /**
+ * Simplifies the string tree by e.g. flattening nested [[StringTreeOr]] instances.
+ *
+ * @return The simplified string tree or the same instance if nothing could be simplified.
+ *
+ * @see [[CachedSimplifyNode]]
+ */
+ def simplify: StringTreeNode
+
+ /**
+ * @return The constancy level of the string tree.
+ *
+ * @see [[StringConstancyLevel]]
+ */
+ def constancyLevel: StringConstancyLevel
+
+ /**
+ * The indices of any method parameter references using [[StringTreeParameter]] within the string tree.
+ */
+ lazy val parameterIndices: Set[Int] = children.flatMap(_.parameterIndices).toSet
+
+ /**
+ * Replaces all [[StringTreeParameter]] instances in the string tree that represent a parameter index defined in the
+ * given map with the replacement value for that index. Keeps [[StringTreeParameter]] instances whose their index is
+ * not defined in the map.
+ *
+ * @param parameters A map from parameter indices to replacement values
+ * @return The modified string tree if something could be replaced or the same instance otherwise.
+ */
+ final def replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = {
+ if (parameters.isEmpty ||
+ parameterIndices.isEmpty ||
+ parameters.keySet.intersect(parameterIndices).isEmpty
+ )
+ this
+ else
+ _replaceParameters(parameters)
+ }
+ protected def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode
+
+ /**
+ * @return True if this string tree node represents an empty string, false otherwise.
+ */
+ def isEmpty: Boolean = false
+
+ /**
+ * @return True if this string tree node represents no string, false otherwise.
+ */
+ def isInvalid: Boolean = false
+}
+
+object StringTreeNode {
+
+ def lb: StringTreeNode = StringTreeDynamicString
+ def ub: StringTreeNode = StringTreeInvalidElement
+}
+
+sealed trait CachedSimplifyNode extends StringTreeNode {
+
+ private var _simplified = false
+ final def simplify: StringTreeNode = {
+ if (_simplified) {
+ this
+ } else {
+ _simplify match {
+ case cr: CachedSimplifyNode =>
+ cr._simplified = true
+ cr
+ case r => r
+ }
+ }
+ }
+
+ protected def _simplify: StringTreeNode
+}
+
+sealed trait CachedHashCode extends Product {
+
+ private lazy val _hashCode = scala.util.hashing.MurmurHash3.productHash(this)
+ override def hashCode(): Int = _hashCode
+ override def canEqual(obj: Any): Boolean = obj.hashCode() == _hashCode
+}
+
+/**
+ * Represents the concatenation of all its children.
+ */
+case class StringTreeConcat(override val children: Seq[StringTreeNode]) extends CachedSimplifyNode with CachedHashCode {
+
+ override protected def _toRegex: String = {
+ children.size match {
+ case 0 => throw new IllegalStateException("Tried to convert StringTreeConcat with no children to a regex!")
+ case 1 => children.head.toRegex
+ case _ => s"${children.map(_.toRegex).reduceLeft((o, n) => s"$o$n")}"
+ }
+ }
+
+ override def sorted: StringTreeNode = StringTreeConcat(children.map(_.sorted))
+
+ override def _simplify: StringTreeNode = {
+ val nonEmptyChildren = children.map(_.simplify).filterNot(_.isEmpty)
+ if (nonEmptyChildren.exists(_.isInvalid)) {
+ StringTreeInvalidElement
+ } else {
+ nonEmptyChildren.size match {
+ case 0 => StringTreeEmptyConst
+ case 1 => nonEmptyChildren.head
+ case _ =>
+ var newChildren = Seq.empty[StringTreeNode]
+ nonEmptyChildren.foreach {
+ case concatChild: StringTreeConcat => newChildren :++= concatChild.children
+ case child => newChildren :+= child
+ }
+ StringTreeConcat(newChildren)
+ }
+ }
+ }
+
+ override def constancyLevel: StringConstancyLevel =
+ children.map(_.constancyLevel).reduceLeft(StringConstancyLevel.determineForConcat)
+
+ def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = {
+ val childrenWithChange = children.map { c =>
+ val nc = c.replaceParameters(parameters)
+ (nc, c ne nc)
+ }
+
+ if (childrenWithChange.exists(_._2)) {
+ StringTreeConcat(childrenWithChange.map(_._1))
+ } else {
+ this
+ }
+ }
+
+ def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = {
+ if (targetDepth == 1)
+ replacement
+ else
+ StringTreeConcat(children.map(_.replaceAtDepth(targetDepth - 1, replacement)))
+ }
+}
+
+object StringTreeConcat {
+ def fromNodes(children: StringTreeNode*): StringTreeNode = {
+ if (children.isEmpty || children.exists(_.isInvalid)) {
+ StringTreeInvalidElement
+ } else if (children.forall(_.isEmpty)) {
+ StringTreeEmptyConst
+ } else {
+ new StringTreeConcat(children.filterNot(_.isEmpty))
+ }
+ }
+}
+
+/**
+ * Represents the free choice between all its children.
+ */
+trait StringTreeOr extends CachedSimplifyNode with CachedHashCode {
+
+ protected val _children: Iterable[StringTreeNode]
+
+ override final lazy val children: Seq[StringTreeNode] = _children.toSeq
+
+ override protected def _toRegex: String = {
+ children.size match {
+ case 0 => throw new IllegalStateException("Tried to convert StringTreeOr with no children to a regex!")
+ case 1 => children.head.toRegex
+ case _ => s"(${children.map(_.toRegex).reduceLeft((o, n) => s"$o|$n")})"
+ }
+ }
+
+ override def constancyLevel: StringConstancyLevel =
+ _children.map(_.constancyLevel).reduceLeft(StringConstancyLevel.meet)
+
+ override lazy val parameterIndices: Set[Int] = _children.flatMap(_.parameterIndices).toSet
+}
+
+object StringTreeOr {
+
+ def apply(children: Seq[StringTreeNode]): StringTreeNode = apply(children.toSet)
+
+ def apply(children: Set[StringTreeNode]): StringTreeNode = {
+ if (children.isEmpty) {
+ StringTreeInvalidElement
+ } else if (children.size == 1) {
+ children.head
+ } else {
+ new SetBasedStringTreeOr(children)
+ }
+ }
+
+ def fromNodes(children: StringTreeNode*): StringTreeNode = SetBasedStringTreeOr.createWithSimplify(children.toSet)
+}
+
+/**
+ * @inheritdoc
+ *
+ * Based on a [[Seq]] for children storage. To be used if the order of children is important for e.g. reduction to a
+ * regex and subsequent comparison to another string tree.
+ */
+private case class SeqBasedStringTreeOr(override val _children: Seq[StringTreeNode]) extends StringTreeOr {
+
+ override def sorted: StringTreeNode = SeqBasedStringTreeOr(children.map(_.sorted).sortBy(_.toRegex))
+
+ override def _simplify: StringTreeNode = {
+ val validChildren = _children.map(_.simplify).filterNot(_.isInvalid)
+ validChildren.size match {
+ case 0 => StringTreeInvalidElement
+ case 1 => validChildren.head
+ case _ =>
+ val newChildren = validChildren.flatMap {
+ case orChild: StringTreeOr => orChild.children
+ case child => Set(child)
+ }
+ val distinctNewChildren = newChildren.distinct
+ distinctNewChildren.size match {
+ case 1 => distinctNewChildren.head
+ case _ => SeqBasedStringTreeOr(distinctNewChildren)
+ }
+ }
+ }
+
+ def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = {
+ val childrenWithChange = _children.map { c =>
+ val nc = c.replaceParameters(parameters)
+ (nc, c ne nc)
+ }
+
+ if (childrenWithChange.exists(_._2)) {
+ SeqBasedStringTreeOr(childrenWithChange.map(_._1))
+ } else {
+ this
+ }
+ }
+
+ def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = {
+ if (targetDepth == 1)
+ replacement
+ else
+ SeqBasedStringTreeOr(children.map(_.replaceAtDepth(targetDepth - 1, replacement)))
+ }
+}
+
+object SeqBasedStringTreeOr {
+
+ def apply(children: Seq[StringTreeNode]): StringTreeNode = {
+ if (children.isEmpty) {
+ StringTreeInvalidElement
+ } else if (children.size == 1) {
+ children.head
+ } else {
+ new SeqBasedStringTreeOr(children)
+ }
+ }
+}
+
+/**
+ * @inheritdoc
+ *
+ * Based on a [[Set]] for children storage. To be used if the order of children is NOT important.
+ */
+case class SetBasedStringTreeOr(override val _children: Set[StringTreeNode]) extends StringTreeOr {
+
+ override lazy val depth: Int = _children.map(_.depth).maxOption.getOrElse(0) + 1
+
+ override def sorted: StringTreeNode = SeqBasedStringTreeOr(children.map(_.sorted).sortBy(_.toRegex))
+
+ override def _simplify: StringTreeNode = SetBasedStringTreeOr._simplifySelf {
+ _children.map(_.simplify).filterNot(_.isInvalid)
+ }
+
+ def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = {
+ val childrenWithChange = _children.map { c =>
+ val nc = c.replaceParameters(parameters)
+ (nc, c ne nc)
+ }
+
+ if (childrenWithChange.exists(_._2)) {
+ SetBasedStringTreeOr(childrenWithChange.map(_._1))
+ } else {
+ this
+ }
+ }
+
+ def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = {
+ if (targetDepth == 1)
+ replacement
+ else
+ SetBasedStringTreeOr(_children.map(_.replaceAtDepth(targetDepth - 1, replacement)))
+ }
+}
+
+object SetBasedStringTreeOr {
+
+ def apply(children: Set[StringTreeNode]): StringTreeNode = {
+ if (children.isEmpty) {
+ StringTreeInvalidElement
+ } else if (children.size == 1) {
+ children.head
+ } else {
+ new SetBasedStringTreeOr(children)
+ }
+ }
+
+ def createWithSimplify(children: Set[StringTreeNode]): StringTreeNode =
+ _simplifySelf(children.filterNot(_.isInvalid))
+
+ private def _simplifySelf(_children: Set[StringTreeNode]): StringTreeNode = {
+ _children.size match {
+ case 0 => StringTreeInvalidElement
+ case 1 => _children.head
+ case _ =>
+ val newChildrenBuilder = Set.newBuilder[StringTreeNode]
+ _children.foreach {
+ case setOrChild: SetBasedStringTreeOr => newChildrenBuilder.addAll(setOrChild._children)
+ case orChild: StringTreeOr => newChildrenBuilder.addAll(orChild.children)
+ case child => newChildrenBuilder.addOne(child)
+ }
+ val newChildren = newChildrenBuilder.result()
+ newChildren.size match {
+ case 1 => newChildren.head
+ case _ => SetBasedStringTreeOr(newChildren)
+ }
+ }
+ }
+}
+
+/**
+ * Represents a string tree leaf, i.e. a node having no children and can thus return itself during sorting and
+ * simplification.
+ */
+sealed trait SimpleStringTreeNode extends StringTreeNode {
+
+ override final val children: Seq[StringTreeNode] = Seq.empty
+
+ override final def sorted: StringTreeNode = this
+ override final def simplify: StringTreeNode = this
+
+ override def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = this
+ override def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = {
+ if (targetDepth == 1)
+ replacement
+ else
+ replaceAtDepth(targetDepth - 1, replacement)
+ }
+}
+
+case class StringTreeConst(string: String) extends SimpleStringTreeNode {
+ override protected def _toRegex: String = Regex.quoteReplacement(string).replaceAll("\\[", "\\\\[")
+
+ override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Constant
+
+ def isIntConst: Boolean = Try(string.toInt).isSuccess
+
+ override def isEmpty: Boolean = string == ""
+}
+
+object StringTreeEmptyConst extends StringTreeConst("") {
+
+ override def isEmpty: Boolean = true
+}
+
+/**
+ * A placeholder for a method parameter value. Should be replaced using [[replaceParameters]] before reducing the string
+ * tree to a regex.
+ *
+ * @param index The method parameter index that is being represented.
+ */
+case class StringTreeParameter(index: Int) extends SimpleStringTreeNode {
+ override protected def _toRegex: String = ".*"
+
+ override lazy val parameterIndices: Set[Int] = Set(index)
+
+ override def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode =
+ parameters.getOrElse(index, this)
+
+ override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Dynamic
+}
+
+object StringTreeParameter {
+
+ def forParameterPC(paramPC: Int): StringTreeParameter = {
+ if (paramPC >= -1) {
+ throw new IllegalArgumentException(s"Invalid parameter pc given: $paramPC")
+ }
+ // Parameters start at PC -2 downwards
+ StringTreeParameter(Math.abs(paramPC + 2))
+ }
+}
+
+object StringTreeInvalidElement extends SimpleStringTreeNode {
+ override protected def _toRegex: String = throw new UnsupportedOperationException()
+
+ override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Invalid
+
+ override def isInvalid: Boolean = true
+}
+
+object StringTreeNull extends SimpleStringTreeNode {
+ // IMPROVE Using this element nested in some other element might lead to unexpected results since it contains regex
+ // matching characters for the beginning and end of a string.
+ override protected def _toRegex: String = "^null$"
+
+ override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Constant
+}
+
+object StringTreeDynamicString extends SimpleStringTreeNode {
+ override protected def _toRegex: String = ".*"
+
+ override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Dynamic
+}
+
+object StringTreeDynamicInt extends SimpleStringTreeNode {
+ // IMPROVE Using this element nested in some other element might lead to unexpected results since it contains regex
+ // matching characters for the beginning and end of a string.
+ override protected def _toRegex: String = "^-?\\d+$"
+
+ override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Dynamic
+}
+
+object StringTreeDynamicFloat extends SimpleStringTreeNode {
+ // IMPROVE Using this element nested in some other element might lead to unexpected results since it contains regex
+ // matching characters for the beginning and end of a string.
+ override protected def _toRegex: String = "^-?\\d*\\.{0,1}\\d+$"
+
+ override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Dynamic
+}
diff --git a/OPAL/br/src/test/scala/org/opalj/br/string/StringConstancyLevelTests.scala b/OPAL/br/src/test/scala/org/opalj/br/string/StringConstancyLevelTests.scala
new file mode 100644
index 0000000000..7bd9cb49f9
--- /dev/null
+++ b/OPAL/br/src/test/scala/org/opalj/br/string/StringConstancyLevelTests.scala
@@ -0,0 +1,73 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package br
+package string
+
+import org.scalatest.funsuite.AnyFunSuite
+
+import org.opalj.br.fpcf.properties.string.StringConstancyLevel
+import org.opalj.br.fpcf.properties.string.StringConstancyLevel.Constant
+import org.opalj.br.fpcf.properties.string.StringConstancyLevel.Dynamic
+import org.opalj.br.fpcf.properties.string.StringConstancyLevel.Invalid
+import org.opalj.br.fpcf.properties.string.StringConstancyLevel.PartiallyConstant
+
+/**
+ * Tests for [[StringConstancyLevel]] methods.
+ *
+ * @author Maximilian Rüsch
+ */
+@org.junit.runner.RunWith(classOf[org.scalatestplus.junit.JUnitRunner])
+class StringConstancyLevelTests extends AnyFunSuite {
+
+ test("tests that the more general string constancy level is computed correctly") {
+ // Trivial cases
+ assert(StringConstancyLevel.meet(Invalid, Invalid) == Invalid)
+ assert(StringConstancyLevel.meet(Constant, Constant) == Constant)
+ assert(StringConstancyLevel.meet(PartiallyConstant, PartiallyConstant) == PartiallyConstant)
+ assert(StringConstancyLevel.meet(Dynamic, Dynamic) == Dynamic)
+
+ // <= Constant
+ assert(StringConstancyLevel.meet(Constant, Invalid) == Constant)
+ assert(StringConstancyLevel.meet(Invalid, Constant) == Constant)
+
+ // <= PartiallyConstant
+ assert(StringConstancyLevel.meet(PartiallyConstant, Invalid) == PartiallyConstant)
+ assert(StringConstancyLevel.meet(Invalid, PartiallyConstant) == PartiallyConstant)
+ assert(StringConstancyLevel.meet(PartiallyConstant, Constant) == PartiallyConstant)
+ assert(StringConstancyLevel.meet(Constant, PartiallyConstant) == PartiallyConstant)
+
+ // <= Dynamic
+ assert(StringConstancyLevel.meet(Invalid, Dynamic) == Dynamic)
+ assert(StringConstancyLevel.meet(Dynamic, Invalid) == Dynamic)
+ assert(StringConstancyLevel.meet(Constant, Dynamic) == Dynamic)
+ assert(StringConstancyLevel.meet(Dynamic, Constant) == Dynamic)
+ assert(StringConstancyLevel.meet(PartiallyConstant, Dynamic) == Dynamic)
+ assert(StringConstancyLevel.meet(Dynamic, PartiallyConstant) == Dynamic)
+ }
+
+ test("tests that the string constancy level for concatenation of two levels is computed correctly") {
+ // Trivial cases
+ assert(StringConstancyLevel.determineForConcat(Invalid, Invalid) == Invalid)
+ assert(StringConstancyLevel.determineForConcat(Constant, Constant) == Constant)
+ assert(StringConstancyLevel.determineForConcat(PartiallyConstant, PartiallyConstant) == PartiallyConstant)
+ assert(StringConstancyLevel.determineForConcat(Dynamic, Dynamic) == Dynamic)
+
+ // Invalid blocks everything
+ assert(StringConstancyLevel.determineForConcat(Constant, Invalid) == Invalid)
+ assert(StringConstancyLevel.determineForConcat(Invalid, Constant) == Invalid)
+ assert(StringConstancyLevel.determineForConcat(PartiallyConstant, Invalid) == Invalid)
+ assert(StringConstancyLevel.determineForConcat(Invalid, PartiallyConstant) == Invalid)
+ assert(StringConstancyLevel.determineForConcat(Invalid, Dynamic) == Invalid)
+ assert(StringConstancyLevel.determineForConcat(Dynamic, Invalid) == Invalid)
+
+ // PartiallyConstant can be retained
+ assert(StringConstancyLevel.determineForConcat(PartiallyConstant, Constant) == PartiallyConstant)
+ assert(StringConstancyLevel.determineForConcat(Constant, PartiallyConstant) == PartiallyConstant)
+ assert(StringConstancyLevel.determineForConcat(PartiallyConstant, Dynamic) == PartiallyConstant)
+ assert(StringConstancyLevel.determineForConcat(Dynamic, PartiallyConstant) == PartiallyConstant)
+
+ // PartiallyConstant can be constructed
+ assert(StringConstancyLevel.determineForConcat(Constant, Dynamic) == PartiallyConstant)
+ assert(StringConstancyLevel.determineForConcat(Dynamic, Constant) == PartiallyConstant)
+ }
+}
diff --git a/OPAL/tac/src/main/resources/reference.conf b/OPAL/tac/src/main/resources/reference.conf
index b21ce227c9..3f4707d64f 100644
--- a/OPAL/tac/src/main/resources/reference.conf
+++ b/OPAL/tac/src/main/resources/reference.conf
@@ -112,9 +112,34 @@ org.opalj {
description = "Produces points-to sets for the results of reflection methods like Class.getMethod.",
eagerFactory = "org.opalj.tac.fpcf.analyses.pointsto.ReflectionAllocationsAnalysisScheduler"
},
+ "StringAnalysis" {
+ description = "Computes strings available on individual variables in the context of the method they are defined in.",
+ lazyFactory = "org.opalj.tac.fpcf.analyses.string.LazyStringAnalysis"
+ },
+ "MethodStringFlowAnalysis" {
+ description = "Computes results of data flow analysis for string variables in the context of a method.",
+ lazyFactory = "org.opalj.tac.fpcf.analyses.string.flowanalysis.MethodStringFlowAnalysis"
+ },
+ "L0StringFlowAnalysis" {
+ description = "Computes string flow functions on level L0 for individual statements in the context of a method to be used in data flow analysis.",
+ lazyFactory = "org.opalj.tac.fpcf.analyses.string.l0.LazyL0StringFlowAnalysis"
+ },
+ "L1StringFlowAnalysis" {
+ description = "Computes string flow functions on level L1 for individual statements in the context of a method to be used in data flow analysis.",
+ lazyFactory = "org.opalj.tac.fpcf.analyses.string.l1.LazyL1StringFlowAnalysis"
+ },
+ "L2StringFlowAnalysis" {
+ description = "Computes string flow functions on level L2 for individual statements in the context of a method to be used in data flow analysis.",
+ lazyFactory = "org.opalj.tac.fpcf.analyses.string.l2.LazyL2StringFlowAnalysis",
+ default = true
+ },
+ "L3StringFlowAnalysis" {
+ description = "Computes string flow functions on level L3 for individual statements in the context of a method to be used in data flow analysis.",
+ lazyFactory = "org.opalj.tac.fpcf.analyses.string.l3.LazyL3StringFlowAnalysis"
+ },
"SystemPropertiesAnalysis" {
- description = "Computes Strings available from system properties.",
- triggeredFactory = "org.opalj.tac.fpcf.analyses.SystemPropertiesAnalysisScheduler"
+ description = "Computes strings available from all instances of the Properties class.",
+ triggeredFactory = "org.opalj.tac.fpcf.analyses.systemproperties.TriggeredSystemPropertiesAnalysisScheduler"
}
}
},
@@ -1529,7 +1554,14 @@ org.opalj {
mergeExceptions = true
},
cg.reflection.ReflectionRelatedCallsAnalysis.highSoundness = "" // e.g. "all" or "class,method",
- fieldaccess.reflection.ReflectionRelatedFieldAccessesAnalysis.highSoundness = false
+ fieldaccess.reflection.ReflectionRelatedFieldAccessesAnalysis.highSoundness = false,
+ string {
+ highSoundness = false,
+ StringAnalysis.depthThreshold = 10
+ MethodStringFlowAnalysis.excludedPackages = [
+ "sun.nio.cs" // Due to problems with the size of the static initialization in the charsets
+ ]
+ }
}
},
tac.cg {
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/DUVar.scala b/OPAL/tac/src/main/scala/org/opalj/tac/DUVar.scala
index 88bcddd54b..abf4b5de8e 100644
--- a/OPAL/tac/src/main/scala/org/opalj/tac/DUVar.scala
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/DUVar.scala
@@ -10,6 +10,7 @@ import org.opalj.ai.pcOfMethodExternalException
import org.opalj.br.ComputationalType
import org.opalj.br.ComputationalTypeReturnAddress
import org.opalj.br.PDUVar
+import org.opalj.br.PDVar
import org.opalj.br.PUVar
import org.opalj.collection.immutable.IntTrieSet
import org.opalj.value.ValueInformation
@@ -187,7 +188,9 @@ class DVar[+Value <: ValueInformation /*org.opalj.ai.ValuesDomain#DomainValue*/
s"DVar(useSites=${useSites.mkString("{", ",", "}")},value=$value,origin=$origin)"
}
- override def toPersistentForm(implicit stmts: Array[Stmt[V]]): Nothing = throw new UnsupportedOperationException
+ override def toPersistentForm(
+ implicit stmts: Array[Stmt[V]]
+ ): PDVar[Value] = PDVar(value, usedBy.map(pcOfDefSite _))
}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/PDUWeb.scala b/OPAL/tac/src/main/scala/org/opalj/tac/PDUWeb.scala
new file mode 100644
index 0000000000..12b638b9f8
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/PDUWeb.scala
@@ -0,0 +1,38 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+
+import org.opalj.br.PDVar
+import org.opalj.br.PUVar
+import org.opalj.collection.immutable.IntTrieSet
+
+/**
+ * Identifies a variable inside a given fixed method. A methods webs can be constructed through the maximal unions of
+ * all intersecting DU-UD-chains of the method.
+ *
+ * @param defPCs The def PCs of the variable that is identified through this web.
+ * @param usePCs The use PCs of the variable that is identified through this web.
+ *
+ * @author Maximilian Rüsch
+ */
+case class PDUWeb(
+ defPCs: IntTrieSet,
+ usePCs: IntTrieSet
+) {
+ def identifiesSameVarAs(other: PDUWeb): Boolean = other.defPCs.intersect(defPCs).nonEmpty
+
+ def combine(other: PDUWeb): PDUWeb = PDUWeb(other.defPCs ++ defPCs, other.usePCs ++ usePCs)
+
+ // Performance optimizations
+ private lazy val _hashCode = scala.util.hashing.MurmurHash3.productHash(this)
+ override def hashCode(): Int = _hashCode
+ override def equals(obj: Any): Boolean = obj.hashCode() == _hashCode && super.equals(obj)
+}
+
+object PDUWeb {
+
+ def apply(pc: Int, pv: PV): PDUWeb = pv match {
+ case pdVar: PDVar[_] => PDUWeb(IntTrieSet(pc), pdVar.usePCs)
+ case puVar: PUVar[_] => PDUWeb(puVar.defPCs, IntTrieSet(pc))
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/SystemPropertiesAnalysisScheduler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/SystemPropertiesAnalysisScheduler.scala
deleted file mode 100644
index e35ccd3c9e..0000000000
--- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/SystemPropertiesAnalysisScheduler.scala
+++ /dev/null
@@ -1,163 +0,0 @@
-/* BSD 2-Clause License - see OPAL/LICENSE for details. */
-package org.opalj
-package tac
-package fpcf
-package analyses
-
-import org.opalj.br.Method
-import org.opalj.br.ObjectType
-import org.opalj.br.analyses.DeclaredMethodsKey
-import org.opalj.br.analyses.ProjectInformationKeys
-import org.opalj.br.analyses.SomeProject
-import org.opalj.br.fpcf.BasicFPCFTriggeredAnalysisScheduler
-import org.opalj.br.fpcf.properties.SystemProperties
-import org.opalj.br.fpcf.properties.cg.Callers
-import org.opalj.fpcf.EOptionP
-import org.opalj.fpcf.EPK
-import org.opalj.fpcf.EPS
-import org.opalj.fpcf.InterimEP
-import org.opalj.fpcf.InterimEUBP
-import org.opalj.fpcf.InterimPartialResult
-import org.opalj.fpcf.PartialResult
-import org.opalj.fpcf.ProperPropertyComputationResult
-import org.opalj.fpcf.PropertyBounds
-import org.opalj.fpcf.PropertyKey
-import org.opalj.fpcf.PropertyStore
-import org.opalj.fpcf.Results
-import org.opalj.fpcf.UBP
-import org.opalj.tac.cg.TypeIteratorKey
-import org.opalj.tac.fpcf.analyses.cg.ReachableMethodAnalysis
-import org.opalj.tac.fpcf.properties.TACAI
-import org.opalj.value.ValueInformation
-
-class SystemPropertiesAnalysisScheduler private[analyses] (
- final val project: SomeProject
-) extends ReachableMethodAnalysis {
-
- def processMethod(
- callContext: ContextType,
- tacaiEP: EPS[Method, TACAI]
- ): ProperPropertyComputationResult = {
- assert(tacaiEP.hasUBP && tacaiEP.ub.tac.isDefined)
- val stmts = tacaiEP.ub.tac.get.stmts
-
- var propertyMap: Map[String, Set[String]] = Map.empty
-
- for (stmt <- stmts) stmt match {
- case VirtualFunctionCallStatement(call)
- if (call.name == "setProperty" || call.name == "put") && classHierarchy.isSubtypeOf(
- call.declaringClass,
- ObjectType("java/util/Properties")
- ) =>
- propertyMap = computeProperties(propertyMap, call.params, stmts)
- case StaticMethodCall(_, ObjectType.System, _, "setProperty", _, params) =>
- propertyMap = computeProperties(propertyMap, params, stmts)
- case _ =>
- }
-
- if (propertyMap.isEmpty) {
- return Results()
- }
-
- def update(
- currentVal: EOptionP[SomeProject, SystemProperties]
- ): Option[InterimEP[SomeProject, SystemProperties]] = currentVal match {
- case UBP(ub) =>
- var oldProperties = ub.properties
- val noNewProperty = propertyMap.forall {
- case (key, values) =>
- oldProperties.contains(key) && {
- val oldValues = oldProperties(key)
- values.forall(oldValues.contains)
- }
- }
-
- if (noNewProperty) {
- None
- } else {
- for ((key, values) <- propertyMap) {
- val oldValues = oldProperties.getOrElse(key, Set.empty)
- oldProperties = oldProperties.updated(key, oldValues ++ values)
- }
- Some(InterimEUBP(project, new SystemProperties(propertyMap)))
- }
-
- case _: EPK[SomeProject, SystemProperties] =>
- Some(InterimEUBP(project, new SystemProperties(propertyMap)))
- }
-
- if (tacaiEP.isFinal) {
- PartialResult[SomeProject, SystemProperties](
- project,
- SystemProperties.key,
- update
- )
- } else {
- InterimPartialResult(
- project,
- SystemProperties.key,
- update,
- Set(tacaiEP),
- continuationForTAC(callContext.method)
- )
- }
- }
-
- def computeProperties(
- propertyMap: Map[String, Set[String]],
- params: Seq[Expr[DUVar[ValueInformation]]],
- stmts: Array[Stmt[DUVar[ValueInformation]]]
- ): Map[String, Set[String]] = {
- var res = propertyMap
-
- assert(params.size == 2)
- val possibleKeys = getPossibleStrings(params.head, stmts)
- val possibleValues = getPossibleStrings(params(1), stmts)
-
- for (key <- possibleKeys) {
- val values = res.getOrElse(key, Set.empty)
- res = res.updated(key, values ++ possibleValues)
- }
-
- res
- }
-
- def getPossibleStrings(
- value: Expr[DUVar[ValueInformation]],
- stmts: Array[Stmt[DUVar[ValueInformation]]]
- ): Set[String] = {
- value.asVar.definedBy filter { index => index >= 0 && stmts(index).asAssignment.expr.isStringConst } map {
- stmts(_).asAssignment.expr.asStringConst.value
- }
- }
-
-}
-
-object SystemPropertiesAnalysisScheduler extends BasicFPCFTriggeredAnalysisScheduler {
-
- override def requiredProjectInformation: ProjectInformationKeys =
- Seq(DeclaredMethodsKey, TypeIteratorKey)
-
- override def uses: Set[PropertyBounds] = Set(
- PropertyBounds.ub(Callers),
- PropertyBounds.ub(TACAI)
- )
-
- override def triggeredBy: PropertyKey[Callers] = Callers.key
-
- override def register(
- p: SomeProject,
- ps: PropertyStore,
- unused: Null
- ): SystemPropertiesAnalysisScheduler = {
- val analysis = new SystemPropertiesAnalysisScheduler(p)
- ps.registerTriggeredComputation(triggeredBy, analysis.analyze)
- analysis
- }
-
- override def derivesEagerly: Set[PropertyBounds] = Set.empty
-
- override def derivesCollaboratively: Set[PropertyBounds] = Set(
- PropertyBounds.ub(SystemProperties)
- )
-}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysis.scala
new file mode 100644
index 0000000000..cc4b1ba544
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysis.scala
@@ -0,0 +1,402 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+
+import scala.collection.mutable.ListBuffer
+
+import org.opalj.br.DeclaredMethod
+import org.opalj.br.Method
+import org.opalj.br.PDVar
+import org.opalj.br.PUVar
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.ContextProviderKey
+import org.opalj.br.fpcf.FPCFAnalysis
+import org.opalj.br.fpcf.analyses.ContextProvider
+import org.opalj.br.fpcf.properties.Context
+import org.opalj.br.fpcf.properties.cg.Callers
+import org.opalj.br.fpcf.properties.cg.NoCallers
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.br.fpcf.properties.string.StringTreeConst
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.br.fpcf.properties.string.StringTreeOr
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.EPK
+import org.opalj.fpcf.EPS
+import org.opalj.fpcf.EUBP
+import org.opalj.fpcf.InterimResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.Result
+import org.opalj.fpcf.SomeEPS
+import org.opalj.fpcf.UBP
+import org.opalj.log.Error
+import org.opalj.log.Info
+import org.opalj.log.OPALLogger.logOnce
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.MethodStringFlow
+
+/**
+ * Base trait for all string analyses that compute results for the FPCF [[StringConstancyProperty]].
+ *
+ * @author Maximilian Rüsch
+ */
+trait StringAnalysis extends FPCFAnalysis with StringAnalysisConfig {
+
+ private final val ConfigLogCategory = "analysis configuration - string analysis"
+
+ /**
+ * @see [[StringAnalysis.DepthThresholdConfigKey]]
+ */
+ protected val depthThreshold: Int = {
+ val depthThreshold =
+ try {
+ project.config.getInt(StringAnalysis.DepthThresholdConfigKey)
+ } catch {
+ case t: Throwable =>
+ logOnce(Error(ConfigLogCategory, s"couldn't read: ${StringAnalysis.DepthThresholdConfigKey}", t))
+ 10
+ }
+
+ logOnce(Info(ConfigLogCategory, "using depth threshold " + depthThreshold))
+ depthThreshold
+ }
+}
+
+object StringAnalysis {
+
+ /**
+ * The string tree depth after which the string analysis does not continue and returns either the current tree
+ * or the tree limited to the depth threshold, depending on the soundness mode in use.
+ */
+ final val DepthThresholdConfigKey = "org.opalj.fpcf.analyses.string.StringAnalysis.depthThreshold"
+}
+
+/**
+ * Analyzes a given variable in context of its method in a context free manner, i.e. without trying to resolve method
+ * parameter references present in the string tree computed for the variable.
+ *
+ * @note This particular instance of the analysis also tries to resolve string constants without involving the
+ * [[MethodStringFlow]] FPCF property in an effort to maintain scalability. Once a variable is known to require
+ * flow-sensitive information, a [[MethodStringFlow]] property is requested from the property store and all
+ * subsequent computations are made simply in reaction to changes in the method string flow dependee.
+ *
+ * @see [[org.opalj.tac.fpcf.analyses.string.flowanalysis.MethodStringFlowAnalysis]], [[ContextStringAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+private[string] class ContextFreeStringAnalysis(override val project: SomeProject) extends StringAnalysis {
+
+ def analyze(vd: VariableDefinition): ProperPropertyComputationResult = {
+ implicit val state: ContextFreeStringAnalysisState = ContextFreeStringAnalysisState(vd, ps(vd.m, TACAI.key))
+
+ if (state.tacaiDependee.isRefinable) {
+ InterimResult(
+ state.entity,
+ StringConstancyProperty.lb,
+ StringConstancyProperty.ub,
+ state.dependees,
+ continuation(state)
+ )
+ } else if (state.tacaiDependee.ub.tac.isEmpty) {
+ // No TAC available, e.g., because the method has no body
+ Result(
+ state.entity,
+ if (highSoundness) StringConstancyProperty.lb
+ else StringConstancyProperty.ub
+ )
+ } else {
+ computeResults
+ }
+ }
+
+ private def continuation(state: ContextFreeStringAnalysisState)(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case _ if eps.pk == TACAI.key =>
+ state.tacaiDependee = eps.asInstanceOf[EOptionP[Method, TACAI]]
+ computeResults(state)
+
+ case _ if eps.pk == MethodStringFlow.key =>
+ state.stringFlowDependee = Some(eps.asInstanceOf[EOptionP[Method, MethodStringFlow]])
+ computeResults(state)
+
+ case _ =>
+ throw new IllegalArgumentException(s"Unexpected eps in continuation: $eps")
+ }
+ }
+
+ private def computeResults(implicit state: ContextFreeStringAnalysisState): ProperPropertyComputationResult = {
+ val newUB = StringConstancyProperty(
+ if (state.stringFlowDependee.isEmpty) computeUBTreeUsingTACAI
+ else computeUBTreeUsingStringFlow
+ )
+
+ if (state.hasDependees && !state.hitDepthThreshold) {
+ InterimResult(
+ state.entity,
+ StringConstancyProperty.lb,
+ newUB,
+ state.dependees,
+ continuation(state)
+ )
+ } else {
+ Result(state.entity, newUB)
+ }
+ }
+
+ private def computeUBTreeUsingTACAI(implicit state: ContextFreeStringAnalysisState): StringTreeNode = {
+ val tac = state.tacaiDependee.ub.tac.get
+
+ def mapDefPCToStringTree(defPC: Int): Option[StringTreeNode] = {
+ if (defPC < 0) {
+ None
+ } else {
+ tac.stmts(valueOriginOfPC(defPC, tac.pcToIndex).get).asAssignment.expr match {
+ case StringConst(_, v) => Some(StringTreeConst(v))
+ case _ => None
+ }
+ }
+ }
+
+ val treeOpts = state.entity.pv match {
+ case PUVar(_, defPCs) =>
+ defPCs.map(pc => mapDefPCToStringTree(pc))
+
+ case PDVar(_, _) =>
+ Set(mapDefPCToStringTree(state.entity.pc))
+ }
+
+ if (treeOpts.exists(_.isEmpty)) {
+ // Could not resolve all values immediately (i.e. non-literal strings), so revert to computing string flow
+ state.stringFlowDependee = Some(ps(state.entity.m, MethodStringFlow.key))
+ computeUBTreeUsingStringFlow
+ } else {
+ StringTreeOr(treeOpts.map(_.get))
+ }
+ }
+
+ private def computeUBTreeUsingStringFlow(implicit state: ContextFreeStringAnalysisState): StringTreeNode = {
+ if (state.stringFlowDependee.isEmpty) {
+ throw new IllegalStateException(s"Requested to compute an UB using method string flow but none is given!")
+ }
+
+ state.stringFlowDependee.get match {
+ case UBP(methodStringFlow) =>
+ val tree = methodStringFlow(state.entity.pc, state.entity.pv).simplify
+ if (tree.depth >= depthThreshold) {
+ // String constancy information got too complex, abort. This guard can probably be removed once
+ // recursing functions are properly handled using e.g. the widen-converge approach.
+ state.hitDepthThreshold = true
+ if (highSoundness) {
+ tree.replaceAtDepth(depthThreshold, StringTreeNode.lb)
+ } else {
+ // In low soundness, we cannot decrease the matched string values by limiting the string tree
+ // with the upper bound. We should also not limit it with the lower bound, since that would
+ // make the string tree at least partially dynamic and cause other low soundness analysis to
+ // abort. Thus, return the tree itself as the final value.
+ tree
+ }
+ } else {
+ tree
+ }
+ case _: EPK[_, MethodStringFlow] =>
+ StringTreeNode.ub
+ }
+ }
+}
+
+/**
+ * Analyzes a given variable in context of its method in a context-sensitive manner, i.e. resolving all method
+ * parameter references given in the string tree that was resolved for the variable from the
+ * [[ContextFreeStringAnalysis]].
+ *
+ * @see [[ContextFreeStringAnalysis]], [[MethodParameterContextStringAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+class ContextStringAnalysis(override val project: SomeProject) extends StringAnalysis {
+
+ def analyze(vc: VariableContext): ProperPropertyComputationResult = {
+ val vdScp = ps(VariableDefinition(vc.pc, vc.pv, vc.m), StringConstancyProperty.key)
+
+ implicit val state: ContextStringAnalysisState = ContextStringAnalysisState(vc, vdScp)
+ state.parameterIndices.foreach { index =>
+ val mpcDependee = ps(MethodParameterContext(index, state.entity.context), StringConstancyProperty.key)
+ state.registerParameterDependee(mpcDependee)
+ }
+
+ computeResults
+ }
+
+ private def continuation(state: ContextStringAnalysisState)(eps: SomeEPS): ProperPropertyComputationResult = {
+ implicit val _state: ContextStringAnalysisState = state
+ eps match {
+ // "Downwards" dependency
+ case EUBP(_: VariableDefinition, _: StringConstancyProperty) =>
+ handleStringDefinitionUpdate(eps.asInstanceOf[EPS[VariableDefinition, StringConstancyProperty]])
+ computeResults
+
+ // "Upwards" dependency
+ case EUBP(_: MethodParameterContext, _: StringConstancyProperty) =>
+ state.updateParamDependee(eps.asInstanceOf[EOptionP[MethodParameterContext, StringConstancyProperty]])
+ computeResults
+
+ case _ =>
+ throw new IllegalArgumentException(s"Unexpected eps in continuation: $eps")
+ }
+ }
+
+ private def handleStringDefinitionUpdate(
+ newDefinitionEPS: EPS[VariableDefinition, StringConstancyProperty]
+ )(implicit state: ContextStringAnalysisState): Unit = {
+ val previousIndices = state.parameterIndices
+ state.updateStringDependee(newDefinitionEPS)
+ val newIndices = state.parameterIndices
+
+ newIndices.diff(previousIndices).foreach { index =>
+ val mpcDependee = ps(MethodParameterContext(index, state.entity.context), StringConstancyProperty.key)
+ state.registerParameterDependee(mpcDependee)
+ }
+ }
+
+ private def computeResults(implicit state: ContextStringAnalysisState): ProperPropertyComputationResult = {
+ if (state.hasDependees) {
+ InterimResult(
+ state.entity,
+ StringConstancyProperty.lb,
+ StringConstancyProperty(state.currentTreeUB),
+ state.dependees,
+ continuation(state)
+ )
+ } else {
+ Result(state.entity, StringConstancyProperty(state.currentTreeUB))
+ }
+ }
+}
+
+/**
+ * Analyzes the possible values of a given method parameter by analyzing the actual method parameters at all call sites
+ * of the given method using the [[ContextStringAnalysis]]. Uses the call graph to find all call sites of the given
+ * method.
+ *
+ * @see [[ContextStringAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+private[string] class MethodParameterContextStringAnalysis(override val project: SomeProject) extends StringAnalysis {
+
+ private implicit val contextProvider: ContextProvider = project.get(ContextProviderKey)
+
+ def analyze(mpc: MethodParameterContext): ProperPropertyComputationResult = {
+ implicit val state: MethodParameterContextStringAnalysisState = MethodParameterContextStringAnalysisState(mpc)
+
+ val callersEOptP = ps(state.dm, Callers.key)
+ state.updateCallers(callersEOptP)
+ if (callersEOptP.hasUBP) {
+ handleNewCallers(NoCallers, callersEOptP.ub)
+ }
+
+ computeResults
+ }
+
+ private def continuation(state: MethodParameterContextStringAnalysisState)(
+ eps: SomeEPS
+ ): ProperPropertyComputationResult = {
+ implicit val _state: MethodParameterContextStringAnalysisState = state
+ eps match {
+ case UBP(callers: Callers) =>
+ val oldCallers = if (state.callersDependee.get.hasUBP) state.callersDependee.get.ub else NoCallers
+ state.updateCallers(eps.asInstanceOf[EOptionP[DeclaredMethod, Callers]])
+ handleNewCallers(oldCallers, callers)
+ computeResults
+
+ case EUBP(m: Method, tacai: TACAI) =>
+ if (tacai.tac.isEmpty) {
+ state.discoveredUnknownTAC = true
+ } else {
+ state.getCallerContexts(m).foreach(handleTACForContext(tacai.tac.get, _))
+ }
+ computeResults
+
+ case EUBP(_: VariableContext, _: StringConstancyProperty) =>
+ state.updateParamDependee(eps.asInstanceOf[EOptionP[VariableContext, StringConstancyProperty]])
+ computeResults
+
+ case _ =>
+ throw new IllegalArgumentException(s"Unexpected eps in continuation: $eps")
+ }
+ }
+
+ private def handleNewCallers(
+ oldCallers: Callers,
+ newCallers: Callers
+ )(implicit state: MethodParameterContextStringAnalysisState): Unit = {
+ val relevantCallerContexts = ListBuffer.empty[(Context, Int)]
+ newCallers.forNewCallerContexts(oldCallers, state.dm) { (_, callerContext, pc, _) =>
+ if (callerContext.hasContext && callerContext.method.hasSingleDefinedMethod) {
+ relevantCallerContexts.append((callerContext, pc))
+ }
+ }
+
+ for { callerContext <- relevantCallerContexts } {
+ state.addCallerContext(callerContext)
+ val tacEOptP = state.getTacaiForContext(callerContext)
+ if (tacEOptP.hasUBP) {
+ val tacOpt = tacEOptP.ub.tac
+ if (tacOpt.isEmpty) {
+ state.discoveredUnknownTAC = true
+ } else {
+ handleTACForContext(tacOpt.get, callerContext)
+ }
+ }
+ }
+ }
+
+ private def handleTACForContext(
+ tac: TAC,
+ context: (Context, Int)
+ )(implicit state: MethodParameterContextStringAnalysisState): Unit = {
+ val callExpr = tac.stmts(valueOriginOfPC(context._2, tac.pcToIndex).get) match {
+ case Assignment(_, _, expr) if expr.isInstanceOf[Call[_]] => expr.asInstanceOf[Call[V]]
+ case ExprStmt(_, expr) if expr.isInstanceOf[Call[_]] => expr.asInstanceOf[Call[V]]
+ case call: Call[_] => call.asInstanceOf[Call[V]]
+ case node => throw new IllegalArgumentException(s"Unexpected argument: $node")
+ }
+
+ val previousCallExpr = state.addCallExprInformationForContext(context, callExpr)
+ if (previousCallExpr.isEmpty || previousCallExpr.get != callExpr) {
+ handleCallExpr(context, callExpr)
+ }
+ }
+
+ private def handleCallExpr(
+ callerContext: (Context, Int),
+ callExpr: Call[V]
+ )(implicit state: MethodParameterContextStringAnalysisState): Unit = {
+ val dm = callerContext._1.method.asDefinedMethod
+ val tac = state.getTACForContext(callerContext)
+ val paramVC = VariableContext(
+ callerContext._2,
+ callExpr.params(state.index).asVar.toPersistentForm(tac.stmts),
+ callerContext._1
+ )
+ state.registerParameterDependee(dm, ps(paramVC, StringConstancyProperty.key))
+ }
+
+ private def computeResults(implicit
+ state: MethodParameterContextStringAnalysisState
+ ): ProperPropertyComputationResult = {
+ if (state.hasDependees) {
+ InterimResult(
+ state.entity,
+ StringConstancyProperty.lb,
+ StringConstancyProperty(state.currentTreeUB),
+ state.dependees,
+ continuation(state)
+ )
+ } else {
+ Result(state.entity, StringConstancyProperty(state.currentTreeUB))
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisConfig.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisConfig.scala
new file mode 100644
index 0000000000..8809d06b75
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisConfig.scala
@@ -0,0 +1,46 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+
+import org.opalj.br.analyses.SomeProject
+import org.opalj.log.Error
+import org.opalj.log.Info
+import org.opalj.log.LogContext
+import org.opalj.log.OPALLogger.logOnce
+
+/**
+ * Shared config between the multiple analyses of the string analysis package.
+ *
+ * @author Maximilian Rüsch
+ */
+trait StringAnalysisConfig {
+
+ val project: SomeProject
+ implicit def logContext: LogContext
+
+ private final val ConfigLogCategory = "analysis configuration - string analysis - universal"
+
+ implicit val highSoundness: Boolean = {
+ val isHighSoundness =
+ try {
+ project.config.getBoolean(StringAnalysisConfig.HighSoundnessConfigKey)
+ } catch {
+ case t: Throwable =>
+ logOnce {
+ Error(ConfigLogCategory, s"couldn't read: ${StringAnalysisConfig.HighSoundnessConfigKey}", t)
+ }
+ false
+ }
+
+ logOnce(Info(ConfigLogCategory, s"using ${if (isHighSoundness) "high" else "low"} soundness mode"))
+ isHighSoundness
+ }
+}
+
+object StringAnalysisConfig {
+
+ final val HighSoundnessConfigKey = "org.opalj.fpcf.analyses.string.highSoundness"
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisScheduler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisScheduler.scala
new file mode 100644
index 0000000000..05017ab7a0
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisScheduler.scala
@@ -0,0 +1,75 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.ContextProviderKey
+import org.opalj.br.fpcf.FPCFAnalysis
+import org.opalj.br.fpcf.FPCFAnalysisScheduler
+import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.fpcf.Entity
+import org.opalj.fpcf.PropertyBounds
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.properties.string.MethodStringFlow
+
+/**
+ * A trait for FPCF analysis schedulers that combine the parts of the string analysis that produce
+ * [[StringConstancyProperty]]s.
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait StringAnalysisScheduler extends FPCFAnalysisScheduler {
+
+ final def derivedProperty: PropertyBounds = PropertyBounds.lub(StringConstancyProperty)
+
+ override def uses: Set[PropertyBounds] = Set(PropertyBounds.ub(MethodStringFlow))
+
+ override final type InitializationData =
+ (ContextFreeStringAnalysis, ContextStringAnalysis, MethodParameterContextStringAnalysis)
+
+ override def init(p: SomeProject, ps: PropertyStore): InitializationData = (
+ new ContextFreeStringAnalysis(p),
+ new ContextStringAnalysis(p),
+ new MethodParameterContextStringAnalysis(p)
+ )
+
+ override def beforeSchedule(p: SomeProject, ps: PropertyStore): Unit = {}
+
+ override def afterPhaseScheduling(ps: PropertyStore, analysis: FPCFAnalysis): Unit = {}
+
+ override def afterPhaseCompletion(p: SomeProject, ps: PropertyStore, analysis: FPCFAnalysis): Unit = {}
+}
+
+/**
+ * A lazy adaptation of the [[StringAnalysisScheduler]]. All three string analyses are combined since the property store
+ * does not allow registering more than one lazy analysis scheduler for a given property.
+ *
+ * @author Maximilian Rüsch
+ */
+object LazyStringAnalysis
+ extends StringAnalysisScheduler with FPCFLazyAnalysisScheduler {
+
+ override def register(p: SomeProject, ps: PropertyStore, data: InitializationData): FPCFAnalysis = {
+ ps.registerLazyPropertyComputation(
+ StringConstancyProperty.key,
+ (entity: Entity) => {
+ entity match {
+ case vd: VariableDefinition => data._1.analyze(vd)
+ case vc: VariableContext => data._2.analyze(vc)
+ case vc: MethodParameterContext => data._3.analyze(vc)
+ case e => throw new IllegalArgumentException(s"Cannot process entity $e")
+ }
+ }
+ )
+ data._1
+ }
+
+ override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty)
+
+ override def requiredProjectInformation: ProjectInformationKeys = Seq(ContextProviderKey)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisState.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisState.scala
new file mode 100644
index 0000000000..e6eac2b89d
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisState.scala
@@ -0,0 +1,233 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+
+import scala.collection.mutable
+
+import org.opalj.br.DeclaredMethod
+import org.opalj.br.DefinedMethod
+import org.opalj.br.Method
+import org.opalj.br.fpcf.properties.Context
+import org.opalj.br.fpcf.properties.cg.Callers
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.br.fpcf.properties.string.StringTreeOr
+import org.opalj.fpcf.Entity
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.EPS
+import org.opalj.fpcf.Property
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.MethodStringFlow
+
+/**
+ * FPCF analysis state for the [[ContextFreeStringAnalysis]].
+ *
+ * @see [[ContextFreeStringAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+private[string] case class ContextFreeStringAnalysisState(
+ entity: VariableDefinition,
+ var tacaiDependee: EOptionP[Method, TACAI],
+ var stringFlowDependee: Option[EOptionP[Method, MethodStringFlow]] = None,
+ var hitDepthThreshold: Boolean = false
+) {
+
+ def hasDependees: Boolean = tacaiDependee.isRefinable || stringFlowDependee.exists(_.isRefinable)
+
+ def dependees: Set[EOptionP[Entity, Property]] =
+ Set(tacaiDependee).filter(_.isRefinable) ++ stringFlowDependee.filter(_.isRefinable)
+}
+
+/**
+ * FPCF analysis state for the [[ContextStringAnalysis]]. Provides support for adding required parameter dependees due
+ * to changes in the string dependee at runtime.
+ *
+ * @see [[ContextStringAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+private[string] class ContextStringAnalysisState private (
+ val entity: VariableContext,
+ private var _stringDependee: EOptionP[VariableDefinition, StringConstancyProperty],
+ private var _parameterIndices: Set[Int]
+) {
+
+ def updateStringDependee(stringDependee: EPS[VariableDefinition, StringConstancyProperty]): Unit = {
+ _stringDependee = stringDependee
+ _parameterIndices ++= stringDependee.ub.tree.parameterIndices
+ }
+ def parameterIndices: Set[Int] = _parameterIndices
+
+ // Parameter StringConstancy
+ private val _paramDependees: mutable.Map[MethodParameterContext, EOptionP[
+ MethodParameterContext,
+ StringConstancyProperty
+ ]] =
+ mutable.Map.empty
+
+ def registerParameterDependee(dependee: EOptionP[MethodParameterContext, StringConstancyProperty]): Unit = {
+ if (_paramDependees.contains(dependee.e)) {
+ throw new IllegalArgumentException(s"Tried to register the same parameter dependee twice: $dependee")
+ }
+
+ updateParamDependee(dependee)
+ }
+ def updateParamDependee(dependee: EOptionP[MethodParameterContext, StringConstancyProperty]): Unit =
+ _paramDependees(dependee.e) = dependee
+
+ def currentTreeUB: StringTreeNode = {
+ if (_stringDependee.hasUBP) {
+ val paramTrees = _paramDependees.map { kv =>
+ (
+ kv._1.index,
+ if (kv._2.hasUBP) kv._2.ub.tree
+ else StringTreeNode.ub
+ )
+ }.toMap
+
+ _stringDependee.ub.tree.replaceParameters(paramTrees).simplify
+ } else {
+ StringTreeNode.ub
+ }
+ }
+
+ def hasDependees: Boolean = _stringDependee.isRefinable || _paramDependees.valuesIterator.exists(_.isRefinable)
+
+ def dependees: Set[EOptionP[Entity, Property]] = {
+ val preliminaryDependees =
+ _paramDependees.valuesIterator.filter(_.isRefinable) ++
+ Some(_stringDependee).filter(_.isRefinable)
+
+ preliminaryDependees.toSet
+ }
+}
+
+object ContextStringAnalysisState {
+
+ def apply(
+ entity: VariableContext,
+ stringDependee: EOptionP[VariableDefinition, StringConstancyProperty]
+ ): ContextStringAnalysisState = {
+ val parameterIndices = if (stringDependee.hasUBP) stringDependee.ub.tree.parameterIndices
+ else Set.empty[Int]
+ new ContextStringAnalysisState(entity, stringDependee, parameterIndices)
+ }
+}
+
+/**
+ * FPCF analysis state for the [[MethodParameterContextStringAnalysis]]. Provides support for finding call sites for a
+ * given method as well as the relevant TACAI properties and their call parameter string dependees.
+ *
+ * @see [[MethodParameterContextStringAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+private[string] case class MethodParameterContextStringAnalysisState(
+ entity: MethodParameterContext
+) {
+
+ def dm: DeclaredMethod = entity.context.method
+ def index: Int = entity.index
+
+ // Callers
+ private[string] type CallerContext = (Context, Int)
+ private var _callersDependee: Option[EOptionP[DeclaredMethod, Callers]] = None
+ private val _callerContexts: mutable.Map[CallerContext, Option[Call[V]]] = mutable.Map.empty
+ private val _callerContextsByMethod: mutable.Map[Method, Seq[CallerContext]] = mutable.Map.empty
+
+ def callersDependee: Option[EOptionP[DeclaredMethod, Callers]] = _callersDependee
+
+ def updateCallers(newCallers: EOptionP[DeclaredMethod, Callers]): Unit = _callersDependee = Some(newCallers)
+
+ def addCallerContext(callerContext: CallerContext): Unit = {
+ _callerContexts.update(callerContext, None)
+ _callerContextsByMethod.updateWith(callerContext._1.method.definedMethod) {
+ case None => Some(Seq(callerContext))
+ case Some(prev) => Some(callerContext +: prev)
+ }
+ }
+ def getCallerContexts(m: Method): Seq[CallerContext] = _callerContextsByMethod(m)
+ def addCallExprInformationForContext(callerContext: CallerContext, expr: Call[V]): Option[Call[V]] =
+ _callerContexts.put(callerContext, Some(expr)).flatten
+
+ // TACAI
+ private val _tacaiDependees: mutable.Map[Method, EOptionP[Method, TACAI]] = mutable.Map.empty
+ var discoveredUnknownTAC: Boolean = false
+
+ def updateTacaiDependee(tacEOptP: EOptionP[Method, TACAI]): Unit = _tacaiDependees(tacEOptP.e) = tacEOptP
+ def getTacaiForContext(callerContext: CallerContext)(implicit ps: PropertyStore): EOptionP[Method, TACAI] = {
+ val m = callerContext._1.method.definedMethod
+
+ if (_tacaiDependees.contains(m)) {
+ _tacaiDependees(m)
+ } else {
+ val tacEOptP = ps(m, TACAI.key)
+ _tacaiDependees(tacEOptP.e) = tacEOptP
+ tacEOptP
+ }
+ }
+ def getTACForContext(callerContext: CallerContext)(implicit ps: PropertyStore): TAC =
+ getTacaiForContext(callerContext).ub.tac.get
+
+ // Parameter StringConstancy
+ private val _methodToEntityMapping: mutable.Map[DefinedMethod, Seq[VariableContext]] = mutable.Map.empty
+ private val _paramDependees: mutable.Map[VariableContext, EOptionP[VariableContext, StringConstancyProperty]] =
+ mutable.Map.empty
+
+ def registerParameterDependee(
+ dm: DefinedMethod,
+ dependee: EOptionP[VariableContext, StringConstancyProperty]
+ ): Unit = {
+ if (_paramDependees.contains(dependee.e)) {
+ throw new IllegalArgumentException(s"Tried to register the same parameter dependee twice: $dependee")
+ }
+
+ _methodToEntityMapping.updateWith(dm) {
+ case None => Some(Seq(dependee.e))
+ case Some(previous) => Some((previous :+ dependee.e).sortBy(_.pc))
+ }
+
+ updateParamDependee(dependee)
+ }
+ def updateParamDependee(dependee: EOptionP[VariableContext, StringConstancyProperty]): Unit =
+ _paramDependees(dependee.e) = dependee
+
+ def currentTreeUB(implicit highSoundness: Boolean): StringTreeNode = {
+ var paramOptions = _methodToEntityMapping.keys.toSeq
+ .sortBy(_.id)
+ .flatMap { dm =>
+ _methodToEntityMapping(dm)
+ .map(_paramDependees)
+ .filter(_.hasUBP)
+ .map(_.ub.tree)
+ }
+
+ if (highSoundness && (
+ discoveredUnknownTAC ||
+ _callersDependee.exists(cd => cd.hasUBP && cd.ub.hasCallersWithUnknownContext)
+ )
+ ) {
+ paramOptions :+= StringTreeNode.lb
+ }
+
+ StringTreeOr(paramOptions).simplify
+ }
+
+ def hasDependees: Boolean = _callersDependee.exists(_.isRefinable) ||
+ _tacaiDependees.valuesIterator.exists(_.isRefinable) ||
+ _paramDependees.valuesIterator.exists(_.isRefinable)
+
+ def dependees: Set[EOptionP[Entity, Property]] = {
+ val preliminaryDependees =
+ _tacaiDependees.valuesIterator.filter(_.isRefinable) ++
+ _paramDependees.valuesIterator.filter(_.isRefinable) ++
+ _callersDependee.filter(_.isRefinable)
+
+ preliminaryDependees.toSet
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringInterpreter.scala
new file mode 100644
index 0000000000..5ab76d3eec
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringInterpreter.scala
@@ -0,0 +1,129 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.fpcf.FinalEP
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.Result
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunction
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * The base trait for all string interpreters, producing a FPCF [[StringFlowFunctionProperty]] for a given statement
+ * in the context of its method.
+ *
+ * @author Maximilian Rüsch
+ */
+trait StringInterpreter {
+
+ type T <: Stmt[V]
+
+ /**
+ * @param instr The instruction that is to be interpreted.
+ * @return A [[ProperPropertyComputationResult]] for the given pc containing the interpretation of the given
+ * instruction in the form of a [[StringFlowFunctionProperty]].
+ */
+ def interpret(instr: T)(implicit state: InterpretationState): ProperPropertyComputationResult
+
+ protected[this] def failureTree(implicit highSoundness: Boolean): StringTreeNode =
+ StringInterpreter.failureTree
+
+ protected[this] def failure(v: PV)(implicit state: InterpretationState, highSoundness: Boolean): Result =
+ StringInterpreter.failure(v)
+
+ protected[this] def computeFinalResult(web: PDUWeb, sff: StringFlowFunction)(implicit
+ state: InterpretationState
+ ): Result =
+ StringInterpreter.computeFinalResult(web, sff)
+
+ protected[this] def computeFinalResult(webs: Set[PDUWeb], sff: StringFlowFunction)(implicit
+ state: InterpretationState
+ ): Result =
+ StringInterpreter.computeFinalResult(webs, sff)
+
+ protected[this] def computeFinalResult(p: StringFlowFunctionProperty)(implicit state: InterpretationState): Result =
+ StringInterpreter.computeFinalResult(p)
+}
+
+object StringInterpreter {
+
+ def failureTree(implicit highSoundness: Boolean): StringTreeNode = {
+ if (highSoundness) StringTreeNode.lb
+ else StringTreeNode.ub
+ }
+
+ def failure(v: V)(implicit state: InterpretationState, highSoundness: Boolean): Result =
+ failure(v.toPersistentForm(state.tac.stmts))
+
+ def failure(pv: PV)(implicit state: InterpretationState, highSoundness: Boolean): Result =
+ computeFinalResult(StringFlowFunctionProperty.constForVariableAt(state.pc, pv, failureTree))
+
+ def computeFinalResult(web: PDUWeb, sff: StringFlowFunction)(implicit state: InterpretationState): Result =
+ computeFinalResult(StringFlowFunctionProperty(web, sff))
+
+ def computeFinalResult(webs: Set[PDUWeb], sff: StringFlowFunction)(implicit state: InterpretationState): Result =
+ computeFinalResult(StringFlowFunctionProperty(webs, sff))
+
+ def computeFinalResult(p: StringFlowFunctionProperty)(implicit state: InterpretationState): Result =
+ Result(FinalEP(InterpretationHandler.getEntity(state), p))
+}
+
+/**
+ * Base trait for all [[StringInterpreter]]s that have to evaluate parameters at a given call site, thus providing
+ * appropriate utility.
+ *
+ * @author Maximilian Rüsch
+ */
+trait ParameterEvaluatingStringInterpreter extends StringInterpreter {
+
+ protected def getParametersForPC(pc: Int)(implicit state: InterpretationState): Seq[Expr[V]] = {
+ state.tac.stmts(state.tac.pcToIndex(pc)) match {
+ case ExprStmt(_, vfc: FunctionCall[V]) => vfc.params
+ case Assignment(_, _, fc: FunctionCall[V]) => fc.params
+ case _ => Seq.empty
+ }
+ }
+}
+
+/**
+ * Base trait for all string interpreters that only process [[AssignmentLikeStmt]]s, allowing the trait to pre-unpack
+ * the expression of the [[AssignmentLikeStmt]].
+ *
+ * @author Maximilian Rüsch
+ */
+trait AssignmentLikeBasedStringInterpreter extends StringInterpreter {
+
+ type E <: Expr[V]
+
+ override type T <: AssignmentLikeStmt[V]
+
+ override final def interpret(instr: T)(implicit state: InterpretationState): ProperPropertyComputationResult =
+ interpretExpr(instr, instr.expr.asInstanceOf[E])
+
+ def interpretExpr(instr: T, expr: E)(implicit state: InterpretationState): ProperPropertyComputationResult
+}
+
+/**
+ * Base trait for all string interpreters that only process [[Assignment]]s, allowing the trait to pre-unpack the
+ * assignment target variable as well as the operation performed by [[AssignmentLikeBasedStringInterpreter]].
+ *
+ * @author Maximilian Rüsch
+ */
+trait AssignmentBasedStringInterpreter extends AssignmentLikeBasedStringInterpreter {
+
+ override type T = Assignment[V]
+
+ override final def interpretExpr(instr: T, expr: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ interpretExpr(instr.targetVar.toPersistentForm(state.tac.stmts), expr)
+ }
+
+ def interpretExpr(target: PV, expr: E)(implicit state: InterpretationState): ProperPropertyComputationResult
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/DataFlowAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/DataFlowAnalysis.scala
new file mode 100644
index 0000000000..1505634cab
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/DataFlowAnalysis.scala
@@ -0,0 +1,223 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package flowanalysis
+
+import scala.collection.mutable
+
+import org.opalj.br.fpcf.properties.string.StringTreeDynamicString
+import org.opalj.br.fpcf.properties.string.StringTreeInvalidElement
+import org.opalj.tac.fpcf.properties.string.StringFlowFunction
+import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment
+
+import scalax.collection.GraphTraversal.BreadthFirst
+import scalax.collection.GraphTraversal.Parameters
+
+/**
+ * Performs structural string data flow analysis based on the results of a [[StructuralAnalysis]]. In more detail, this
+ * means that the control tree produced by the [[StructuralAnalysis]] is traversed recursively in a depth-first manner.
+ * Individual regions are processed by piping a [[StringTreeEnvironment]] through their nodes and joining the
+ * environments where paths meet up. Thus, the individual flow functions defined at the statement PCs of a method are
+ * combined using region-type-specific patterns to effectively act as a string flow function of the entire region, which
+ * is then processed itself due to the recursive nature of the algorithm.
+ *
+ * @param controlTree The control tree from the structural analysis.
+ * @param superFlowGraph The super flow graph from the structural analysis
+ * @param highSoundness Whether to use high soundness mode or not. Currently, this influences the handling of loops,
+ * i.e. whether they are approximated by one execution of the loop body (low soundness) or via
+ * "any string" on all variables in the method (high soundness).
+ *
+ * @see [[StructuralAnalysis]], [[StringTreeEnvironment]]
+ *
+ * @author Maximilian Rüsch
+ */
+class DataFlowAnalysis(
+ private val controlTree: ControlTree,
+ private val superFlowGraph: SuperFlowGraph,
+ private val highSoundness: Boolean
+) {
+
+ private val _nodeOrderings = mutable.Map.empty[FlowGraphNode, Seq[SuperFlowGraph#NodeT]]
+ private val _removedBackEdgesGraphs = mutable.Map.empty[FlowGraphNode, (Boolean, SuperFlowGraph)]
+
+ /**
+ * Computes the resulting string tree environment after the data flow analysis.
+ *
+ * @param flowFunctionByPc A mapping from PC to the flow functions to be used
+ * @param startEnv The base environment which is piped into the first region to be processed.
+ * @return The resulting [[StringTreeEnvironment]] after execution of all string flow functions using the region
+ * hierarchy given in the control tree.
+ */
+ def compute(
+ flowFunctionByPc: Map[Int, StringFlowFunction]
+ )(startEnv: StringTreeEnvironment): StringTreeEnvironment = {
+ val startNodeCandidates = controlTree.nodes.filter(!_.hasPredecessors)
+ if (startNodeCandidates.size != 1) {
+ throw new IllegalStateException("Found more than one start node in the control tree!")
+ }
+
+ val startNode = startNodeCandidates.head.outer
+ pipeThroughNode(flowFunctionByPc)(startNode, startEnv)
+ }
+
+ private def pipeThroughNode(flowFunctionByPc: Map[Int, StringFlowFunction])(
+ node: FlowGraphNode,
+ env: StringTreeEnvironment
+ ): StringTreeEnvironment = {
+ val pipe = pipeThroughNode(flowFunctionByPc) _
+ val innerChildNodes = controlTree.get(node).diSuccessors.map(n => superFlowGraph.get(n.outer))
+
+ def processBlock(entry: FlowGraphNode): StringTreeEnvironment = {
+ var currentEnv = env
+ for {
+ currentNode <- superFlowGraph.innerNodeTraverser(
+ superFlowGraph.get(entry),
+ subgraphNodes = innerChildNodes.contains
+ )
+ } {
+ currentEnv = pipe(currentNode.outer, currentEnv)
+ }
+ currentEnv
+ }
+
+ def processIfThenElse(entry: FlowGraphNode): StringTreeEnvironment = {
+ val entryNode = superFlowGraph.get(entry)
+ val successors = entryNode.diSuccessors.intersect(innerChildNodes).map(_.outer).toList.sorted
+ val branches = (successors.head, successors.tail.head)
+
+ val envAfterEntry = pipe(entry, env)
+ val envAfterBranches = (pipe(branches._1, envAfterEntry), pipe(branches._2, envAfterEntry))
+
+ envAfterBranches._1.join(envAfterBranches._2)
+ }
+
+ def processIfThen(entry: FlowGraphNode): StringTreeEnvironment = {
+ val limitedFlowGraph = superFlowGraph.filter(innerChildNodes.contains)
+ val entryNode = limitedFlowGraph.get(entry)
+ val (yesBranch, noBranch) = if (entryNode.diSuccessors.head.diSuccessors.nonEmpty) {
+ (entryNode.diSuccessors.head, entryNode.diSuccessors.tail.head)
+ } else {
+ (entryNode.diSuccessors.tail.head, entryNode.diSuccessors.head)
+ }
+
+ val envAfterEntry = pipe(entry, env)
+ val envAfterBranches = (
+ pipe(yesBranch.diSuccessors.head, pipe(yesBranch, envAfterEntry)),
+ pipe(noBranch, envAfterEntry)
+ )
+
+ envAfterBranches._1.join(envAfterBranches._2)
+ }
+
+ def handleProperSubregion[A <: FlowGraphNode, G <: SuperFlowGraph](
+ g: G,
+ innerNodes: Set[G#NodeT],
+ entry: A
+ ): StringTreeEnvironment = {
+ val entryNode = g.get(entry)
+ val sortedNodes = _nodeOrderings.getOrElseUpdate(
+ node, {
+ val ordering = g.NodeOrdering((in1, in2) => in1.compare(in2))
+ val traverser = entryNode.innerNodeTraverser(Parameters(BreadthFirst))
+ .withOrdering(ordering)
+ .withSubgraph(nodes = innerNodes.contains)
+ // We know that the graph is acyclic here, so we can be sure that the topological sort never fails
+ traverser.topologicalSort().toOption.get.toSeq
+ }
+ )
+
+ val currentNodeEnvs = mutable.Map((entryNode, pipe(entry, env)))
+ for {
+ _currentNode <- sortedNodes.filter(_ != entryNode)
+ currentNode = _currentNode.asInstanceOf[g.NodeT]
+ } {
+ val previousEnvs = currentNode.diPredecessors.toList.map { dp =>
+ pipe(currentNode.outer, currentNodeEnvs(dp))
+ }
+ currentNodeEnvs.update(currentNode, StringTreeEnvironment.joinMany(previousEnvs))
+ }
+
+ currentNodeEnvs(sortedNodes.last.asInstanceOf[g.NodeT])
+ }
+
+ def processProper(entry: FlowGraphNode): StringTreeEnvironment = {
+ handleProperSubregion[FlowGraphNode, superFlowGraph.type](superFlowGraph, innerChildNodes, entry)
+ }
+
+ def processSelfLoop(entry: FlowGraphNode): StringTreeEnvironment = {
+ val resultEnv = pipe(entry, env)
+ // IMPROVE only update affected variables instead of all
+ if (resultEnv != env && highSoundness) env.updateAll(StringTreeDynamicString)
+ else resultEnv
+ }
+
+ def processWhileLoop(entry: FlowGraphNode): StringTreeEnvironment = {
+ val entryNode = superFlowGraph.get(entry)
+ val envAfterEntry = pipe(entry, env)
+
+ superFlowGraph.innerNodeTraverser(entryNode, subgraphNodes = innerChildNodes)
+
+ var resultEnv = env
+ for {
+ currentNode <- superFlowGraph
+ .innerNodeTraverser(entryNode)
+ .withSubgraph(n => n != entryNode && innerChildNodes.contains(n))
+ } {
+ resultEnv = pipe(currentNode.outer, resultEnv)
+ }
+
+ // IMPROVE only update affected variables instead of all
+ if (resultEnv != envAfterEntry && highSoundness) envAfterEntry.updateAll(StringTreeDynamicString)
+ else resultEnv
+ }
+
+ def processNaturalLoop(entry: FlowGraphNode): StringTreeEnvironment = {
+ val (isCyclic, removedBackEdgesGraph) = _removedBackEdgesGraphs.getOrElseUpdate(
+ node, {
+ val limitedFlowGraph = superFlowGraph.filter(innerChildNodes.contains)
+ val entryPredecessors = limitedFlowGraph.get(entry).diPredecessors
+ val computedRemovedBackEdgesGraph = limitedFlowGraph.filterNot(
+ edgeP = edge =>
+ edge.sources.toList.toSet.intersect(entryPredecessors).nonEmpty
+ && edge.targets.contains(limitedFlowGraph.get(entry))
+ )
+ (computedRemovedBackEdgesGraph.isCyclic, computedRemovedBackEdgesGraph)
+ }
+ )
+
+ if (isCyclic) {
+ // IMPROVE only update affected variables instead of all
+ if (highSoundness) env.updateAll(StringTreeDynamicString)
+ else env.updateAll(StringTreeInvalidElement)
+ } else {
+ // Handle resulting acyclic region
+ val resultEnv = handleProperSubregion[FlowGraphNode, removedBackEdgesGraph.type](
+ removedBackEdgesGraph,
+ removedBackEdgesGraph.nodes.toSet,
+ entry
+ )
+ // IMPROVE only update affected variables instead of all
+ if (resultEnv != env && highSoundness) env.updateAll(StringTreeDynamicString)
+ else resultEnv
+ }
+ }
+
+ node match {
+ case Statement(pc) if pc >= 0 => flowFunctionByPc(pc)(env)
+ case Statement(_) => env
+
+ case Region(Block, _, entry) => processBlock(entry)
+ case Region(IfThenElse, _, entry) => processIfThenElse(entry)
+ case Region(IfThen, _, entry) => processIfThen(entry)
+ case Region(Proper, _, entry) => processProper(entry)
+ case Region(SelfLoop, _, entry) => processSelfLoop(entry)
+ case Region(WhileLoop, _, entry) => processWhileLoop(entry)
+ case Region(NaturalLoop, _, entry) => processNaturalLoop(entry)
+
+ case _ => env
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/FlowGraphNode.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/FlowGraphNode.scala
new file mode 100644
index 0000000000..88f4cea03c
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/FlowGraphNode.scala
@@ -0,0 +1,85 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package flowanalysis
+
+sealed trait RegionType extends Product
+
+sealed trait AcyclicRegionType extends RegionType
+sealed trait CyclicRegionType extends RegionType
+
+case object Block extends AcyclicRegionType
+case object IfThen extends AcyclicRegionType
+case object IfThenElse extends AcyclicRegionType
+case object Case extends AcyclicRegionType
+case object Proper extends AcyclicRegionType
+case object SelfLoop extends CyclicRegionType
+case object WhileLoop extends CyclicRegionType
+case object NaturalLoop extends CyclicRegionType
+case object Improper extends CyclicRegionType
+
+/**
+ * A node in a flow graph and control tree produced by the [[StructuralAnalysis]].
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait FlowGraphNode extends Ordered[FlowGraphNode] {
+
+ def nodeIds: Set[Int]
+
+ override def compare(that: FlowGraphNode): Int = nodeIds.toList.min.compare(that.nodeIds.toList.min)
+}
+
+/**
+ * Represents a region of nodes in a [[FlowGraph]], consisting of multiple sub-nodes. Can identify general acyclic and
+ * cyclic structures or more specialised instances of such structures such as [[IfThenElse]] or [[WhileLoop]].
+ *
+ * @param regionType The type of the region.
+ * @param nodeIds The union of all ids the leafs that are contained in this region.
+ * @param entry The direct child of this region that contains the first leaf to be executed when entering the region.
+ */
+case class Region(regionType: RegionType, override val nodeIds: Set[Int], entry: FlowGraphNode) extends FlowGraphNode {
+
+ override def toString: String =
+ s"Region(${regionType.productPrefix}; ${nodeIds.toList.sorted.mkString(",")}; ${entry.toString})"
+
+ // Performance optimizations
+ private lazy val _hashCode = scala.util.hashing.MurmurHash3.productHash(this)
+ override def hashCode(): Int = _hashCode
+ override def canEqual(obj: Any): Boolean = obj.hashCode() == _hashCode
+}
+
+/**
+ * Represents a single statement in a methods [[FlowGraph]] and is one of the leaf nodes to be grouped by a [[Region]].
+ *
+ * @param pc The PC that the statement is given at.
+ */
+case class Statement(pc: Int) extends FlowGraphNode {
+
+ override val nodeIds: Set[Int] = Set(pc)
+
+ override def toString: String = s"Statement($pc)"
+}
+
+/**
+ * An additional global entry node to a methods [[FlowGraph]] to ensure only one entry node exists.
+ */
+object GlobalEntry extends FlowGraphNode {
+
+ override val nodeIds: Set[Int] = Set(Int.MinValue + 1)
+
+ override def toString: String = "GlobalEntry"
+}
+
+/**
+ * An additional global exit node to a methods [[FlowGraph]] to ensure only one exit node exists.
+ */
+object GlobalExit extends FlowGraphNode {
+
+ override val nodeIds: Set[Int] = Set(Int.MinValue)
+
+ override def toString: String = "GlobalExit"
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysis.scala
new file mode 100644
index 0000000000..d35e367220
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysis.scala
@@ -0,0 +1,157 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package flowanalysis
+
+import scala.jdk.CollectionConverters._
+
+import org.opalj.br.Method
+import org.opalj.br.analyses.DeclaredMethods
+import org.opalj.br.analyses.DeclaredMethodsKey
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.FPCFAnalysis
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.EUBP
+import org.opalj.fpcf.FinalEP
+import org.opalj.fpcf.FinalP
+import org.opalj.fpcf.InterimResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.Result
+import org.opalj.fpcf.SomeEPS
+import org.opalj.log.Error
+import org.opalj.log.Info
+import org.opalj.log.OPALLogger.logOnce
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.MethodStringFlow
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * Analyzes a methods string flow results by applying a [[StructuralAnalysis]] to identify all control flow regions of
+ * the methods CFG and subsequently applying a [[DataFlowAnalysis]] to compute a resulting string tree environment
+ * using string flow functions derived from the FPCF [[StringFlowFunctionProperty]].
+ *
+ * @note Packages can be configured to be excluded from analysis entirely due to e.g. size problems. In these cases, the
+ * lower or upper bound string tree environment will be returned, depending on the soundness mode of the analysis.
+ *
+ * @see [[StructuralAnalysis]], [[DataFlowAnalysis]], [[StringFlowFunctionProperty]], [[StringAnalysisConfig]]
+ *
+ * @author Maximilian Rüsch
+ */
+class MethodStringFlowAnalysis(override val project: SomeProject) extends FPCFAnalysis with StringAnalysisConfig {
+
+ private final val ConfigLogCategory = "analysis configuration - method string flow analysis"
+
+ private val excludedPackages: Seq[String] = {
+ val packages =
+ try {
+ project.config.getStringList(MethodStringFlowAnalysis.ExcludedPackagesConfigKey).asScala
+ } catch {
+ case t: Throwable =>
+ logOnce {
+ Error(
+ ConfigLogCategory,
+ s"couldn't read: ${MethodStringFlowAnalysis.ExcludedPackagesConfigKey}",
+ t
+ )
+ }
+ Seq.empty[String]
+ }
+
+ logOnce(Info(ConfigLogCategory, s"${packages.size} packages are excluded from string flow analysis"))
+ packages.toSeq
+ }
+
+ val declaredMethods: DeclaredMethods = project.get(DeclaredMethodsKey)
+
+ def analyze(method: Method): ProperPropertyComputationResult = {
+ val state = MethodStringFlowAnalysisState(method, declaredMethods(method), ps(method, TACAI.key))
+
+ if (excludedPackages.exists(method.classFile.thisType.packageName.startsWith(_))) {
+ Result(
+ state.entity,
+ if (highSoundness) MethodStringFlow.lb
+ else MethodStringFlow.ub
+ )
+ } else if (state.tacDependee.isRefinable) {
+ InterimResult(
+ state.entity,
+ MethodStringFlow.lb,
+ MethodStringFlow.ub,
+ Set(state.tacDependee),
+ continuation(state)
+ )
+ } else if (state.tacDependee.ub.tac.isEmpty) {
+ // No TAC available, e.g., because the method has no body
+ Result(
+ state.entity,
+ if (highSoundness) MethodStringFlow.lb
+ else MethodStringFlow.ub
+ )
+ } else {
+ determinePossibleStrings(state)
+ }
+ }
+
+ private def determinePossibleStrings(implicit
+ state: MethodStringFlowAnalysisState
+ ): ProperPropertyComputationResult = {
+ implicit val tac: TAC = state.tac
+
+ state.flowGraph = FlowGraph(tac.cfg)
+ val (_, superFlowGraph, controlTree) =
+ StructuralAnalysis.analyze(state.flowGraph, FlowGraph.entry)
+ state.superFlowGraph = superFlowGraph
+ state.controlTree = controlTree
+ state.flowAnalysis = new DataFlowAnalysis(state.controlTree, state.superFlowGraph, highSoundness)
+
+ state.flowGraph.nodes.toOuter.foreach {
+ case Statement(pc) if pc >= 0 =>
+ state.updateDependee(pc, propertyStore(MethodPC(pc, state.dm), StringFlowFunctionProperty.key))
+
+ case _ =>
+ }
+
+ computeResults
+ }
+
+ private def continuation(state: MethodStringFlowAnalysisState)(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case FinalP(_: TACAI) if eps.pk.equals(TACAI.key) =>
+ state.tacDependee = eps.asInstanceOf[FinalEP[Method, TACAI]]
+ determinePossibleStrings(state)
+
+ case EUBP(e: MethodPC, _: StringFlowFunctionProperty) if eps.pk.equals(StringFlowFunctionProperty.key) =>
+ state.updateDependee(e.pc, eps.asInstanceOf[EOptionP[MethodPC, StringFlowFunctionProperty]])
+ computeResults(state)
+
+ case _ =>
+ throw new IllegalArgumentException(s"Unknown EPS given in continuation: $eps")
+ }
+ }
+
+ private def computeResults(implicit state: MethodStringFlowAnalysisState): ProperPropertyComputationResult = {
+ if (state.hasDependees) {
+ InterimResult.forUB(
+ state.entity,
+ computeNewUpperBound(state),
+ state.dependees.toSet,
+ continuation(state)
+ )
+ } else {
+ Result(state.entity, computeNewUpperBound(state))
+ }
+ }
+
+ private def computeNewUpperBound(state: MethodStringFlowAnalysisState): MethodStringFlow = {
+ val startEnv = state.getStartEnvAndReset
+ MethodStringFlow(state.flowAnalysis.compute(state.getFlowFunctionsByPC)(startEnv))
+ }
+}
+
+object MethodStringFlowAnalysis {
+
+ final val ExcludedPackagesConfigKey = "org.opalj.fpcf.analyses.string.MethodStringFlowAnalysis.excludedPackages"
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisScheduler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisScheduler.scala
new file mode 100644
index 0000000000..3cddd596e4
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisScheduler.scala
@@ -0,0 +1,58 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package flowanalysis
+
+import org.opalj.br.analyses.DeclaredMethodsKey
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.FPCFAnalysis
+import org.opalj.br.fpcf.FPCFAnalysisScheduler
+import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
+import org.opalj.fpcf.PropertyBounds
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.MethodStringFlow
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * A shared scheduler trait for analyses that analyse the string flow of given methods.
+ *
+ * @see [[MethodStringFlowAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait MethodStringFlowAnalysisScheduler extends FPCFAnalysisScheduler {
+
+ final def derivedProperty: PropertyBounds = PropertyBounds.ub(MethodStringFlow)
+
+ override def uses: Set[PropertyBounds] = Set(
+ PropertyBounds.ub(TACAI),
+ PropertyBounds.ub(StringFlowFunctionProperty)
+ )
+
+ override final type InitializationData = MethodStringFlowAnalysis
+ override def init(p: SomeProject, ps: PropertyStore): InitializationData = new MethodStringFlowAnalysis(p)
+
+ override def beforeSchedule(p: SomeProject, ps: PropertyStore): Unit = {}
+
+ override def afterPhaseScheduling(ps: PropertyStore, analysis: FPCFAnalysis): Unit = {}
+
+ override def afterPhaseCompletion(p: SomeProject, ps: PropertyStore, analysis: FPCFAnalysis): Unit = {}
+}
+
+object LazyMethodStringFlowAnalysis
+ extends MethodStringFlowAnalysisScheduler with FPCFLazyAnalysisScheduler {
+
+ override def register(p: SomeProject, ps: PropertyStore, initData: InitializationData): FPCFAnalysis = {
+ ps.registerLazyPropertyComputation(MethodStringFlow.key, initData.analyze)
+ initData
+ }
+
+ override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty)
+
+ override def requiredProjectInformation: ProjectInformationKeys = Seq(DeclaredMethodsKey)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisState.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisState.scala
new file mode 100644
index 0000000000..cde53ad181
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisState.scala
@@ -0,0 +1,123 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package flowanalysis
+
+import scala.collection.mutable
+
+import org.opalj.ai.ImmediateVMExceptionsOriginOffset
+import org.opalj.br.DefinedMethod
+import org.opalj.br.Method
+import org.opalj.br.fpcf.properties.string.StringTreeInvalidElement
+import org.opalj.br.fpcf.properties.string.StringTreeParameter
+import org.opalj.collection.immutable.IntTrieSet
+import org.opalj.fpcf.EOptionP
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.StringFlowFunction
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment
+
+/**
+ * The state of the [[MethodStringFlowAnalysis]] containing data flow analysis relevant information such as flow graphs
+ * and all string flow dependees for the method under analysis.
+ *
+ * @see [[MethodStringFlowAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+case class MethodStringFlowAnalysisState(entity: Method, dm: DefinedMethod, var tacDependee: EOptionP[Method, TACAI]) {
+
+ def tac: TAC = {
+ if (tacDependee.hasUBP && tacDependee.ub.tac.isDefined)
+ tacDependee.ub.tac.get
+ else
+ throw new IllegalStateException("Cannot get a TAC from a TACAI with no or empty upper bound!")
+ }
+
+ var flowGraph: FlowGraph = _
+ var superFlowGraph: SuperFlowGraph = _
+ var controlTree: ControlTree = _
+ var flowAnalysis: DataFlowAnalysis = _
+
+ private val pcToDependeeMapping: mutable.Map[Int, EOptionP[MethodPC, StringFlowFunctionProperty]] =
+ mutable.Map.empty
+ private val pcToWebChangeMapping: mutable.Map[Int, Boolean] = mutable.Map.empty
+
+ def updateDependee(pc: Int, dependee: EOptionP[MethodPC, StringFlowFunctionProperty]): Unit = {
+ val prevOpt = pcToDependeeMapping.get(pc)
+ if (prevOpt.isEmpty || prevOpt.get.hasNoUBP || (dependee.hasUBP && prevOpt.get.ub.webs != dependee.ub.webs)) {
+ pcToWebChangeMapping.update(pc, true)
+ }
+ pcToDependeeMapping.update(pc, dependee)
+ }
+
+ def dependees: Set[EOptionP[MethodPC, StringFlowFunctionProperty]] =
+ pcToDependeeMapping.values.filter(_.isRefinable).toSet
+
+ def hasDependees: Boolean = pcToDependeeMapping.valuesIterator.exists(_.isRefinable)
+
+ def getFlowFunctionsByPC: Map[Int, StringFlowFunction] = pcToDependeeMapping.toMap.transform { (_, eOptP) =>
+ if (eOptP.hasUBP) eOptP.ub.flow
+ else StringFlowFunctionProperty.ub.flow
+ }
+
+ private def webs: IndexedSeq[PDUWeb] = {
+ pcToDependeeMapping.values.flatMap { v =>
+ if (v.hasUBP) v.ub.webs
+ else StringFlowFunctionProperty.ub.webs
+ }.toIndexedSeq.appendedAll(tac.params.parameters.zipWithIndex.map {
+ case (param, index) =>
+ PDUWeb(IntTrieSet(-index - 1), if (param != null) param.useSites else IntTrieSet.empty)
+ })
+ }
+
+ private var _startEnv: StringTreeEnvironment = StringTreeEnvironment(Map.empty)
+ def getStartEnvAndReset(implicit highSoundness: Boolean): StringTreeEnvironment = {
+ if (pcToWebChangeMapping.exists(_._2)) {
+ val indexedWebs = mutable.ArrayBuffer.empty[PDUWeb]
+ val defPCToWebIndex = mutable.Map.empty[Int, Int]
+ webs.foreach { web =>
+ val existingDefPCs = web.defPCs.filter(defPCToWebIndex.contains)
+ if (existingDefPCs.nonEmpty) {
+ val indices = existingDefPCs.toList.map(defPCToWebIndex).distinct
+ if (indices.size == 1) {
+ val index = indices.head
+ indexedWebs.update(index, indexedWebs(index).combine(web))
+ web.defPCs.foreach(defPCToWebIndex.update(_, index))
+ } else {
+ val newIndex = indices.head
+ val originalWebs = indices.map(indexedWebs)
+ indexedWebs.update(newIndex, originalWebs.reduce(_.combine(_)).combine(web))
+ indices.tail.foreach(indexedWebs.update(_, null))
+ originalWebs.foreach(_.defPCs.foreach(defPCToWebIndex.update(_, newIndex)))
+ web.defPCs.foreach(defPCToWebIndex.update(_, newIndex))
+ }
+ } else {
+ val newIndex = indexedWebs.length
+ indexedWebs.append(web)
+ web.defPCs.foreach(defPCToWebIndex.update(_, newIndex))
+ }
+ }
+
+ val startMap = indexedWebs.filter(_ != null)
+ .map { web: PDUWeb =>
+ val defPC = web.defPCs.toList.min
+
+ if (defPC >= 0) {
+ (web, StringTreeInvalidElement)
+ } else if (defPC < -1 && defPC > ImmediateVMExceptionsOriginOffset) {
+ (web, StringTreeParameter.forParameterPC(defPC))
+ } else {
+ // IMPROVE interpret "this" (parameter pc -1) with reference to String and StringBuilder classes
+ (web, StringInterpreter.failureTree)
+ }
+ }.toMap
+ _startEnv = StringTreeEnvironment(startMap)
+ pcToWebChangeMapping.mapValuesInPlace((_, _) => false)
+ }
+ _startEnv
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/StructuralAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/StructuralAnalysis.scala
new file mode 100644
index 0000000000..27110fd72d
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/StructuralAnalysis.scala
@@ -0,0 +1,456 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package flowanalysis
+
+import scala.collection.mutable
+
+import org.opalj.graphs.DominatorTree
+
+import scalax.collection.GraphTraversal.DepthFirst
+import scalax.collection.GraphTraversal.Parameters
+import scalax.collection.OneOrMore
+import scalax.collection.OuterEdge
+import scalax.collection.edges.DiEdge
+import scalax.collection.generic.Edge
+import scalax.collection.hyperedges.DiHyperEdge
+import scalax.collection.immutable.Graph
+import scalax.collection.mutable.{Graph => MutableGraph}
+
+/**
+ * An algorithm that identifies several different types of flow regions in a given flow graph and reduces them to a
+ * single node iteratively. The algorithm terminates either when a single node is left in the flow graph or such a state
+ * could not be reached after [[maxIterations]].
+ *
+ * On termination, the [[analyze]] function returns:
+ *
+ * -
+ * The reduced flow graph, a single node equal to the root node of the control tree.
+ *
+ * -
+ * A super flow graph as a combination of the given source flow graph and the control tree. For each
+ * node contained in the control tree, the super flow graph contains the node itself and edges to its children
+ * as referenced the control tree. However, its children are still connected with edges as contained in the
+ * source flow graph.
+ *
+ * This representation eases traversal for data flow analysis such as by [[DataFlowAnalysis]].
+ *
+ * -
+ * The control tree, as a hierarchic representation of the control flow regions identified by the algorithm.
+ *
+ *
+ *
+ * This algorithm is adapted from Muchnick, S.S. (1997). Advanced Compiler Design and Implementation and optimized for
+ * performance.
+ *
+ * @see [[MethodStringFlowAnalysis]], [[FlowGraphNode]], [[DataFlowAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+object StructuralAnalysis {
+
+ private final val maxIterations = 1000
+
+ private type MFlowGraph = MutableGraph[FlowGraphNode, DiEdge[FlowGraphNode]]
+ private type MControlTree = MutableGraph[FlowGraphNode, DiEdge[FlowGraphNode]]
+ private type MSuperFlowGraph = MutableGraph[FlowGraphNode, Edge[FlowGraphNode]]
+
+ def analyze(initialGraph: FlowGraph, entry: FlowGraphNode): (FlowGraph, SuperFlowGraph, ControlTree) = {
+ val flowGraph: MFlowGraph = MutableGraph.from(initialGraph.edges.outerIterable)
+ val superFlowGraph: MSuperFlowGraph =
+ MutableGraph.from(initialGraph.edges.outerIterable).asInstanceOf[MSuperFlowGraph]
+ var currentEntry = entry
+ val controlTree: MControlTree = MutableGraph.empty[FlowGraphNode, DiEdge[FlowGraphNode]]
+
+ var (immediateDominators, allDominators) = computeDominators(flowGraph, entry)
+ /**
+ * @return True when the given node n strictly dominates the node w.
+ */
+ def strictlyDominates(n: FlowGraphNode, w: FlowGraphNode): Boolean = n != w && allDominators(w).contains(n)
+
+ val knownPartOfNoCycle = mutable.Set.empty[FlowGraphNode]
+ def inCycle(n: FlowGraphNode): Boolean = {
+ if (knownPartOfNoCycle.contains(n)) {
+ false
+ } else {
+ // IMPROVE if no cycle is found, we can use the visitor of `findCycleContaining` to identify more nodes
+ // that are known to not be part of a cycle and add them to `knownPartOfNoCycle`.
+ val cycleOpt = flowGraph.findCycleContaining(flowGraph.get(n))
+ if (cycleOpt.isDefined) {
+ true
+ } else {
+ knownPartOfNoCycle.add(n)
+ false
+ }
+ }
+ }
+
+ var iterations = 0
+ while (flowGraph.order > 1 && iterations < maxIterations) {
+ // Find post order depth first traversal order for nodes
+ var postCtr = 1
+ val post = mutable.ListBuffer.empty[FlowGraphNode]
+
+ def replace(subNodes: Set[FlowGraphNode], entry: FlowGraphNode, regionType: RegionType): Unit = {
+ val newRegion = Region(regionType, subNodes.flatMap(_.nodeIds), entry)
+
+ // Compact
+ // Note that adding the new region to the graph and superGraph is done anyways since we add edges later
+ val maxPost = post.indexOf(subNodes.maxBy(post.indexOf))
+ post(maxPost) = newRegion
+ // Removing old regions from the graph is done later
+ post.filterInPlace(r => !subNodes.contains(r))
+ postCtr = post.indexOf(newRegion)
+
+ if (subNodes.forall(knownPartOfNoCycle.contains)) {
+ knownPartOfNoCycle.add(newRegion)
+ }
+ knownPartOfNoCycle.subtractAll(subNodes)
+
+ // Replace edges
+ val incomingEdges = flowGraph.edges.filter { e =>
+ !subNodes.contains(e.outer.source) && subNodes.contains(e.outer.target)
+ }
+ val outgoingEdges = flowGraph.edges.filter { e =>
+ subNodes.contains(e.outer.source) && !subNodes.contains(e.outer.target)
+ }
+
+ val newRegionEdges = incomingEdges.map { e =>
+ OuterEdge[FlowGraphNode, DiEdge[FlowGraphNode]](DiEdge(e.outer.source, newRegion))
+ }.concat(outgoingEdges.map { e =>
+ OuterEdge[FlowGraphNode, DiEdge[FlowGraphNode]](DiEdge(newRegion, e.outer.target))
+ })
+ flowGraph.addAll(newRegionEdges)
+ flowGraph.removeAll(subNodes, Set.empty)
+
+ superFlowGraph.addAll(newRegionEdges)
+ superFlowGraph.removeAll {
+ incomingEdges.concat(outgoingEdges).map(e => DiEdge(e.outer.source, e.outer.target))
+ .concat(Seq(DiHyperEdge(OneOrMore(newRegion), OneOrMore.from(subNodes).get)))
+ }
+
+ // Update dominator data
+ val commonDominators = subNodes.map(allDominators).reduce(_.intersect(_))
+ allDominators.subtractAll(subNodes).update(newRegion, commonDominators)
+ allDominators = allDominators.map(kv =>
+ (
+ kv._1, {
+ val index = kv._2.indexWhere(subNodes.contains)
+ if (index != -1)
+ kv._2.patch(index, Seq(newRegion), kv._2.lastIndexWhere(subNodes.contains) - index + 1)
+ else
+ kv._2
+ }
+ )
+ )
+ immediateDominators = allDominators.map(kv => (kv._1, kv._2.head))
+
+ // Update remaining graph state
+ controlTree.addAll(subNodes.map(node =>
+ OuterEdge[FlowGraphNode, DiEdge[FlowGraphNode]](DiEdge(newRegion, node))
+ ))
+ if (subNodes.contains(currentEntry)) {
+ currentEntry = newRegion
+ }
+ }
+
+ // Determine post-order depth-first traversal in the given graph
+ val ordering = flowGraph.NodeOrdering((in1, in2) => in1.compare(in2))
+ flowGraph.innerNodeDownUpTraverser(
+ flowGraph.get(currentEntry),
+ Parameters(DepthFirst),
+ ordering = ordering
+ ).foreach {
+ case (down, in) if !down => post.append(in.outer)
+ case _ =>
+ }
+
+ while (flowGraph.order > 1 && postCtr < post.size) {
+ var n = post(postCtr)
+
+ val gPostMap =
+ post.reverse.zipWithIndex.map(ni =>
+ (flowGraph.get(ni._1).asInstanceOf[MFlowGraph#NodeT], ni._2)
+ ).toMap
+ val (newStartingNode, acyclicRegionOpt) =
+ locateAcyclicRegion[FlowGraphNode, MFlowGraph](flowGraph, gPostMap, allDominators)(n)
+ n = newStartingNode
+ if (acyclicRegionOpt.isDefined) {
+ val (arType, nodes, entry) = acyclicRegionOpt.get
+ replace(nodes, entry, arType)
+ } else if (inCycle(n)) {
+ var reachUnder = Set(n)
+ for {
+ m <- flowGraph.nodes.outerIterator
+ if m != n
+ innerM = controlTree.find(m)
+ if innerM.isEmpty || !innerM.get.hasPredecessors
+ if StructuralAnalysis.pathBack[FlowGraphNode, MFlowGraph](flowGraph, strictlyDominates)(m, n)
+ } {
+ reachUnder = reachUnder.incl(m)
+ }
+
+ val cyclicRegionOpt = locateCyclicRegion[FlowGraphNode, MFlowGraph](flowGraph, n, reachUnder)
+ if (cyclicRegionOpt.isDefined) {
+ val (crType, nodes, entry) = cyclicRegionOpt.get
+ replace(nodes, entry, crType)
+ } else {
+ postCtr += 1
+ }
+ } else {
+ postCtr += 1
+ }
+ }
+
+ iterations += 1
+ }
+
+ if (iterations >= maxIterations) {
+ throw new IllegalStateException(s"Could not reduce tree in $maxIterations iterations!")
+ }
+
+ (
+ Graph.from(flowGraph.edges.outerIterable),
+ Graph.from(superFlowGraph.edges.outerIterable),
+ Graph.from(controlTree.edges.outerIterable)
+ )
+ }
+
+ /**
+ * Computes the immediate and global dominators for each of the nodes in the given graph.
+ *
+ * @param graph The graph to compute dominators for.
+ * @param entry The entry node to the graph under analysis.
+ * @return (A map for immediate dominators by graph node, A map for all dominators of a graph node by graph node)
+ */
+ private def computeDominators[A, G <: MutableGraph[A, DiEdge[A]]](
+ graph: G,
+ entry: A
+ ): (mutable.Map[A, A], mutable.Map[A, Seq[A]]) = {
+ val indexedNodes = graph.nodes.toIndexedSeq
+ val indexOf = indexedNodes.zipWithIndex.toMap
+ val domTree = DominatorTree(
+ indexOf(graph.get(entry)),
+ graph.get(entry).hasPredecessors,
+ index => { f => indexedNodes(index).diSuccessors.foreach(ds => f(indexOf(ds))) },
+ index => { f => indexedNodes(index).diPredecessors.foreach(ds => f(indexOf(ds))) },
+ indexedNodes.size - 1
+ )
+ val outerIndexedNodes = indexedNodes.map(_.outer)
+ val immediateDominators = mutable.Map.from {
+ domTree.immediateDominators.zipWithIndex.map(iDomWithIndex => {
+ (outerIndexedNodes(iDomWithIndex._2), outerIndexedNodes(iDomWithIndex._1))
+ })
+ }
+ immediateDominators.update(entry, entry)
+
+ def getAllDominators(n: A): Seq[A] = {
+ val builder = Seq.newBuilder[A]
+ var c = n
+ while (c != entry) {
+ builder.addOne(c)
+ c = immediateDominators(c)
+ }
+ builder.addOne(entry)
+ builder.result()
+ }
+ val allDominators = immediateDominators.map(kv => (kv._1, getAllDominators(kv._2)))
+
+ (immediateDominators, allDominators)
+ }
+
+ /**
+ * Determines if a path exists from the given node m to some other node k that does not contain the node n AND an
+ * edge k -> n exists that is a back edge in the given graph. The latter is realized by using predecessors to find
+ * eligible edges and using a strict domination predicate to determine a back edge from it.
+ *
+ * @param graph The graph under analysis.
+ * @param strictlyDominates Predicate if a given first node strictly dominates the given second nodes in the given graph.
+ * @param m The node that forms the starting node of the path.
+ * @param n The node that forms the ending node of the path.
+ * @return True if there is path back from m to n over some intermediate node k where m -> k does not contain n.
+ */
+ private def pathBack[A, G <: MutableGraph[A, DiEdge[A]]](graph: G, strictlyDominates: (A, A) => Boolean)(
+ m: A,
+ n: A
+ ): Boolean = {
+ val innerN = graph.get(n)
+ val nonNFromMTraverser = graph.innerNodeTraverser(graph.get(m), subgraphNodes = _ != innerN)
+ val predecessorsOfN = innerN.diPredecessors
+ graph.nodes.exists { innerK =>
+ innerK.outer != n &&
+ predecessorsOfN.contains(innerK) &&
+ strictlyDominates(n, innerK.outer) &&
+ nonNFromMTraverser.pathTo(innerK).isDefined
+ }
+ }
+
+ /**
+ * Identifies a candidate acyclic region if one can be found starting the identification at the given start node.
+ * If no region can be found, a new starting node is returned that can be used to resume searching for other region
+ * types. If a region can be found, all information needed to replace the contained nodes with a new region node in
+ * the graph is returned.
+ *
+ * @param graph The graph under analysis.
+ * @param postOrderTraversal A post order DFS traversal of the given graph as a mapping from node to its DF position.
+ * @param allDominators A dominator index as produced by [[computeDominators]].
+ * @param startingNode The node to start locating the acyclic region at.
+ * @return The new starting node of a candidate region as well as an option of: 1. The type of the found acyclic
+ * region, 2. a set containing all region nodes and 3. the entry node to the region.
+ */
+ private def locateAcyclicRegion[A <: FlowGraphNode, G <: MutableGraph[A, DiEdge[A]]](
+ graph: G,
+ postOrderTraversal: Map[G#NodeT, Int],
+ allDominators: mutable.Map[A, Seq[A]]
+ )(startingNode: A): (A, Option[(AcyclicRegionType, Set[A], A)]) = {
+ var nSet = Set.empty[graph.NodeT]
+ var entry: graph.NodeT = graph.get(startingNode)
+
+ // Expand nSet down
+ var n = graph.get(startingNode)
+ while ((n.outer == startingNode || n.diPredecessors.size == 1) && n.diSuccessors.size == 1) {
+ nSet += n
+ n = n.diSuccessors.head
+ }
+ if (n.diPredecessors.size == 1) {
+ nSet += n
+ }
+
+ // Expand nSet up
+ n = graph.get(startingNode)
+ while (n.diPredecessors.size == 1 && (n.outer == startingNode || n.diSuccessors.size == 1)) {
+ nSet += n
+ entry = n
+ n = n.diPredecessors.head
+ }
+ if (n.diSuccessors.size == 1) {
+ nSet += n
+ entry = n
+ }
+
+ def isAcyclic(nodes: Set[graph.NodeT]): Boolean = {
+ nodes.forall { node =>
+ val postOrderIndex = postOrderTraversal(node)
+ node.diSuccessors.forall { successor =>
+ !nodes.contains(successor) || postOrderTraversal(successor) >= postOrderIndex
+ }
+ }
+ }
+
+ def locateProperAcyclicInterval: Option[AcyclicRegionType] = {
+ val dominatedNodes = allDominators.filter(_._2.contains(n.outer)).map(kv => graph.get(kv._1)).toSet ++ Set(n)
+ if (dominatedNodes.size == 1 ||
+ !isAcyclic(dominatedNodes) ||
+ // Check if no dominated node is reached from a non-dominated node
+ !dominatedNodes.excl(n).forall(_.diPredecessors.subsetOf(dominatedNodes)) ||
+ // Check if all dominated nodes agree on a single successor outside the set (if it exists)
+ dominatedNodes.flatMap(node => node.diSuccessors.diff(dominatedNodes)).size > 1
+ ) {
+ None
+ } else {
+ nSet = dominatedNodes
+ entry = n
+
+ Some(Proper)
+ }
+ }
+
+ val newDirectSuccessors = n.diSuccessors
+ val rType = if (nSet.size > 1) {
+ // Condition is added to ensure chosen bb does not contain any self loops or other cyclic regions
+ // IMPROVE weaken to allow back edges from the "last" nSet member to the first to enable reductions to self loops
+ if (isAcyclic(nSet))
+ Some(Block)
+ else
+ None
+ } else if (newDirectSuccessors.size == 2) {
+ val m = newDirectSuccessors.head
+ val k = newDirectSuccessors.tail.head
+ if (m.diSuccessors.headOption == k.diSuccessors.headOption
+ && m.diSuccessors.size == 1
+ && m.diPredecessors.size == 1
+ && k.diPredecessors.size == 1
+ ) {
+ nSet = Set(n, m, k)
+ entry = n
+ Some(IfThenElse)
+ } else if ((
+ m.diSuccessors.size == 1
+ && m.diSuccessors.head == k
+ && m.diPredecessors.size == 1
+ && k.diPredecessors.size == 2
+ ) || (
+ k.diSuccessors.size == 1
+ && k.diSuccessors.head == m
+ && k.diPredecessors.size == 1
+ && m.diPredecessors.size == 2
+ )
+ ) {
+ nSet = Set(n, m, k)
+ entry = n
+ Some(IfThen)
+ } else {
+ locateProperAcyclicInterval
+ }
+ } else if (newDirectSuccessors.size > 2) {
+ locateProperAcyclicInterval
+ } else {
+ None
+ }
+
+ (n.outer, rType.map((_, nSet.map(_.outer), entry.outer)))
+ }
+
+ /**
+ * Identifies a candidate cyclic region if one can be found starting the identification at the given start node.
+ *
+ * @param graph The graph under analysis.
+ * @param startingNode The node to start identifying cyclic regions at.
+ * @param reachUnder A set of all nodes that can be reached starting from the given starting node that have a path
+ * back to the given start node.
+ * @return An option of: 1. The cyclic region type, 2. the nodes contained in the region and 3. the entry node of
+ * the region.
+ *
+ * @note This implementation does not yet support improper regions and their reduction and will throw upon their
+ * detection.
+ */
+ private def locateCyclicRegion[A, G <: MutableGraph[A, DiEdge[A]]](
+ graph: G,
+ startingNode: A,
+ reachUnder: Set[A]
+ ): Option[(CyclicRegionType, Set[A], A)] = {
+ if (reachUnder.size == 1) {
+ return if (graph.find(DiEdge(startingNode, startingNode)).isDefined)
+ Some((SelfLoop, reachUnder, reachUnder.head))
+ else None
+ }
+
+ if (reachUnder.exists(m => graph.get(startingNode).pathTo(graph.get(m)).isEmpty)) {
+ // IMPROVE reliably detect size of improper regions and reduce
+ throw new IllegalStateException("This implementation of structural analysis cannot handle improper regions!")
+ }
+
+ val m = reachUnder.excl(startingNode).head
+ if (graph.get(startingNode).diPredecessors.size == 2
+ && graph.get(startingNode).diSuccessors.size == 2
+ && graph.get(m).diPredecessors.size == 1
+ && graph.get(m).diSuccessors.size == 1
+ ) {
+ Some((WhileLoop, reachUnder, startingNode))
+ } else {
+ val enteringNodes =
+ reachUnder.filter(graph.get(_).diPredecessors.exists(dp => !reachUnder.contains(dp.outer)))
+
+ if (enteringNodes.size > 1) {
+ throw new IllegalStateException("Found more than one entering node for a natural loop!")
+ } else if (enteringNodes.isEmpty) {
+ throw new IllegalStateException("Found no entering node for a natural loop!")
+ }
+
+ Some((NaturalLoop, reachUnder, enteringNodes.head))
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/package.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/package.scala
new file mode 100644
index 0000000000..9208912ace
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/package.scala
@@ -0,0 +1,174 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+
+import org.opalj.br.cfg.BasicBlock
+import org.opalj.br.cfg.CFG
+
+import scalax.collection.edges.DiEdge
+import scalax.collection.generic.Edge
+import scalax.collection.immutable.Graph
+import scalax.collection.immutable.TypedGraphFactory
+import scalax.collection.io.dot.DotAttr
+import scalax.collection.io.dot.DotAttrStmt
+import scalax.collection.io.dot.DotEdgeStmt
+import scalax.collection.io.dot.DotGraph
+import scalax.collection.io.dot.DotNodeStmt
+import scalax.collection.io.dot.DotRootGraph
+import scalax.collection.io.dot.Elem
+import scalax.collection.io.dot.Graph2DotExport
+import scalax.collection.io.dot.Id
+import scalax.collection.io.dot.NodeId
+
+/**
+ * @author Maximilian Rüsch
+ */
+package object flowanalysis {
+
+ type ControlTree = Graph[FlowGraphNode, DiEdge[FlowGraphNode]]
+ type FlowGraph = Graph[FlowGraphNode, DiEdge[FlowGraphNode]]
+ type SuperFlowGraph = Graph[FlowGraphNode, Edge[FlowGraphNode]]
+
+ object FlowGraph extends TypedGraphFactory[FlowGraphNode, DiEdge[FlowGraphNode]] {
+
+ /**
+ * The entry point of all flow graphs.
+ *
+ * @see [[GlobalEntry]]
+ */
+ val entry: FlowGraphNode = GlobalEntry
+
+ private def mapInstrIndexToPC[V <: Var[V]](cfg: CFG[Stmt[V], TACStmts[V]])(index: Int): Int = {
+ if (index >= 0) cfg.code.instructions(index).pc
+ else index
+ }
+
+ /**
+ * Converts a given CFG to a flow graph with additional global entry and exit nodes.
+ *
+ * @see [[FlowGraphNode]]
+ *
+ * @param cfg The CFG to convert to a flow graph.
+ * @return The flow graph obtained from the CFG.
+ */
+ def apply[V <: Var[V]](cfg: CFG[Stmt[V], TACStmts[V]]): FlowGraph = {
+ val toPC = mapInstrIndexToPC(cfg) _
+
+ val edges = cfg.allNodes.flatMap {
+ case bb: BasicBlock =>
+ val firstNode = Statement(toPC(bb.startPC))
+ var currentEdges = Seq.empty[DiEdge[FlowGraphNode]]
+ if (bb.startPC != bb.endPC) {
+ Range.inclusive(bb.startPC, bb.endPC).tail.foreach { instrIndex =>
+ currentEdges :+= DiEdge(
+ currentEdges.lastOption.map(_.target).getOrElse(firstNode),
+ Statement(toPC(instrIndex))
+ )
+ }
+ }
+
+ val lastNode = if (currentEdges.nonEmpty) currentEdges.last.target
+ else firstNode
+ currentEdges ++ bb.successors.map(s => DiEdge(lastNode, Statement(toPC(s.nodeId))))
+ case n =>
+ n.successors.map(s => DiEdge(Statement(toPC(n.nodeId)), Statement(toPC(s.nodeId))))
+ }.toSet
+ val g = Graph.from(edges + DiEdge(entry, entryFromCFG(cfg)))
+
+ val normalReturnNode = Statement(cfg.normalReturnNode.nodeId)
+ val abnormalReturnNode = Statement(cfg.abnormalReturnNode.nodeId)
+ val hasNormalReturn = cfg.normalReturnNode.predecessors.nonEmpty
+ val hasAbnormalReturn = cfg.abnormalReturnNode.predecessors.nonEmpty
+
+ (hasNormalReturn, hasAbnormalReturn) match {
+ case (true, true) =>
+ g.incl(DiEdge(normalReturnNode, GlobalExit)).incl(DiEdge(abnormalReturnNode, GlobalExit))
+
+ case (true, false) =>
+ g.excl(abnormalReturnNode)
+
+ case (false, true) =>
+ g.excl(normalReturnNode)
+
+ case _ =>
+ throw new IllegalStateException(
+ "Cannot transform a CFG with neither normal nor abnormal return edges!"
+ )
+ }
+ }
+
+ private[this] def entryFromCFG[V <: Var[V]](cfg: CFG[Stmt[V], TACStmts[V]]): Statement =
+ Statement(mapInstrIndexToPC(cfg)(cfg.startBlock.nodeId))
+
+ /**
+ * @param graph The graph consisting of flow graph nodes to convert to DOT format.
+ * @return A DOT string of the graph.
+ */
+ def toDot[N <: FlowGraphNode, E <: Edge[N]](graph: Graph[N, E]): String = {
+ val root = DotRootGraph(
+ directed = true,
+ id = Some(Id("MyDot")),
+ attrStmts = List(DotAttrStmt(Elem.node, List(DotAttr(Id("shape"), Id("record"))))),
+ attrList = List(DotAttr(Id("attr_1"), Id(""""one"""")), DotAttr(Id("attr_2"), Id("")))
+ )
+
+ def edgeTransformer(innerEdge: Graph[N, E]#EdgeT): Option[(DotGraph, DotEdgeStmt)] = {
+ val edge = innerEdge.outer
+ Some(
+ (
+ root,
+ DotEdgeStmt(NodeId(edge.sources.head.toString), NodeId(edge.targets.head.toString))
+ )
+ )
+ }
+
+ def hEdgeTransformer(innerHEdge: Graph[N, E]#EdgeT): Iterable[(DotGraph, DotEdgeStmt)] = {
+ val color = DotAttr(Id("color"), Id(s""""#%06x"""".format(scala.util.Random.nextInt(1 << 24))))
+
+ innerHEdge.outer.targets.toList map (target =>
+ (
+ root,
+ DotEdgeStmt(
+ NodeId(innerHEdge.outer.sources.head.toString),
+ NodeId(target.toString),
+ Seq(color)
+ )
+ )
+ )
+ }
+
+ def nodeTransformer(innerNode: Graph[N, E]#NodeT): Option[(DotGraph, DotNodeStmt)] = {
+ val node = innerNode.outer
+ val attributes = if (node.nodeIds.size == 1) Seq.empty
+ else Seq(
+ DotAttr(Id("style"), Id("filled")),
+ DotAttr(
+ Id("fillcolor"),
+ node match {
+ case Region(_: AcyclicRegionType, _, _) => Id(""""green"""")
+ case Region(_: CyclicRegionType, _, _) => Id(""""purple"""")
+ case _ => Id(""""white"""")
+ }
+ )
+ )
+ Some(
+ (
+ root,
+ DotNodeStmt(NodeId(node.toString), attributes)
+ )
+ )
+ }
+
+ graph.toDot(
+ root,
+ edgeTransformer,
+ hEdgeTransformer = Some(hEdgeTransformer),
+ cNodeTransformer = Some(nodeTransformer),
+ iNodeTransformer = Some(nodeTransformer)
+ )
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationHandler.scala
new file mode 100644
index 0000000000..e361d14ebd
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationHandler.scala
@@ -0,0 +1,81 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package interpretation
+
+import org.opalj.br.Method
+import org.opalj.br.fpcf.FPCFAnalysis
+import org.opalj.fpcf.FinalEP
+import org.opalj.fpcf.InterimResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.SomeEPS
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * Processes expressions that are relevant in order to determine which value(s) the string value at a given def site
+ * might have. Produces string flow functions that transform a given string state during data flow analysis.
+ *
+ * @note [[InterpretationHandler]]s of any level may use [[StringInterpreter]]s from their level or any level below.
+ *
+ * @see [[StringFlowFunctionProperty]], [[org.opalj.tac.fpcf.analyses.string.flowanalysis.DataFlowAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+abstract class InterpretationHandler extends FPCFAnalysis with StringAnalysisConfig {
+
+ def analyze(entity: MethodPC): ProperPropertyComputationResult = {
+ val tacaiEOptP = ps(entity.dm.definedMethod, TACAI.key)
+ implicit val state: InterpretationState = InterpretationState(entity.pc, entity.dm, tacaiEOptP)
+
+ if (tacaiEOptP.isRefinable) {
+ InterimResult(
+ InterpretationHandler.getEntity,
+ StringFlowFunctionProperty.lb,
+ StringFlowFunctionProperty.ub,
+ Set(state.tacDependee),
+ continuation(state)
+ )
+ } else if (tacaiEOptP.ub.tac.isEmpty) {
+ // No TAC available, e.g., because the method has no body
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.constForAll(StringInterpreter.failureTree))
+ } else {
+ processStatementForState
+ }
+ }
+
+ private def continuation(state: InterpretationState)(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case FinalEP(_, _) if eps.pk.equals(TACAI.key) =>
+ state.tacDependee = eps.asInstanceOf[FinalEP[Method, TACAI]]
+ processStatementForState(state)
+
+ case _ =>
+ InterimResult.forUB(
+ InterpretationHandler.getEntity(state),
+ StringFlowFunctionProperty.ub,
+ Set(state.tacDependee),
+ continuation(state)
+ )
+ }
+ }
+
+ private def processStatementForState(implicit state: InterpretationState): ProperPropertyComputationResult = {
+ val defSiteOpt = valueOriginOfPC(state.pc, state.tac.pcToIndex);
+ if (defSiteOpt.isEmpty) {
+ throw new IllegalArgumentException(s"Obtained a pc that does not represent a definition site: ${state.pc}")
+ }
+
+ processStatement(state)(state.tac.stmts(defSiteOpt.get))
+ }
+
+ protected def processStatement(implicit state: InterpretationState): Stmt[V] => ProperPropertyComputationResult
+}
+
+object InterpretationHandler {
+
+ def getEntity(implicit state: InterpretationState): MethodPC = MethodPC(state.pc, state.dm)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationState.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationState.scala
new file mode 100644
index 0000000000..85f41cdfd7
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationState.scala
@@ -0,0 +1,32 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package interpretation
+
+import org.opalj.br.DefinedMethod
+import org.opalj.br.Method
+import org.opalj.fpcf.EOptionP
+import org.opalj.tac.fpcf.properties.TACAI
+
+/**
+ * The state for the FPCF analysis responsible for interpreting the statement at the given PC of the given method and
+ * obtaining its string flow information.
+ *
+ * @see [[InterpretationHandler]], [[StringInterpreter]]
+ *
+ * @param pc The PC of the statement under analysis.
+ * @param dm The method of the statement under analysis.
+ * @param tacDependee The initial TACAI dependee of the method under analysis.
+ */
+case class InterpretationState(pc: Int, dm: DefinedMethod, var tacDependee: EOptionP[Method, TACAI]) {
+
+ def tac: TAC = {
+ if (tacDependee.hasUBP && tacDependee.ub.tac.isDefined)
+ tacDependee.ub.tac.get
+ else
+ throw new IllegalStateException("Cannot get a TAC from a TACAI with no or empty upper bound!")
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/StringFlowAnalysisScheduler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/StringFlowAnalysisScheduler.scala
new file mode 100644
index 0000000000..5bd4f4c63d
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/StringFlowAnalysisScheduler.scala
@@ -0,0 +1,54 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package interpretation
+
+import org.opalj.br.analyses.DeclaredMethodsKey
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.FPCFAnalysis
+import org.opalj.br.fpcf.FPCFAnalysisScheduler
+import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
+import org.opalj.fpcf.PropertyBounds
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * A general scheduler trait for the different string flow analysis levels.
+ *
+ * @see [[InterpretationHandler]]
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait StringFlowAnalysisScheduler extends FPCFAnalysisScheduler {
+
+ final def derivedProperty: PropertyBounds = PropertyBounds.lub(StringFlowFunctionProperty)
+
+ override def uses: Set[PropertyBounds] = PropertyBounds.ubs(TACAI)
+
+ override final type InitializationData = InterpretationHandler
+
+ override def beforeSchedule(p: SomeProject, ps: PropertyStore): Unit = {}
+
+ override def afterPhaseScheduling(ps: PropertyStore, analysis: FPCFAnalysis): Unit = {}
+
+ override def afterPhaseCompletion(p: SomeProject, ps: PropertyStore, analysis: FPCFAnalysis): Unit = {}
+}
+
+trait LazyStringFlowAnalysis
+ extends StringFlowAnalysisScheduler with FPCFLazyAnalysisScheduler {
+
+ override def register(p: SomeProject, ps: PropertyStore, initData: InitializationData): FPCFAnalysis = {
+ ps.registerLazyPropertyComputation(StringFlowFunctionProperty.key, initData.analyze)
+
+ initData
+ }
+
+ override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty)
+
+ override def requiredProjectInformation: ProjectInformationKeys = Seq(DeclaredMethodsKey)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/L0StringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/L0StringAnalysis.scala
new file mode 100644
index 0000000000..bf3d75c2d7
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/L0StringAnalysis.scala
@@ -0,0 +1,32 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l0
+
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.analyses.string.flowanalysis.LazyMethodStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.interpretation.LazyStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l0.interpretation.L0InterpretationHandler
+
+/**
+ * @see [[L0InterpretationHandler]]
+ * @author Maximilian Rüsch
+ */
+object LazyL0StringAnalysis {
+
+ def allRequiredAnalyses: Seq[FPCFLazyAnalysisScheduler] = Seq(
+ LazyStringAnalysis,
+ LazyMethodStringFlowAnalysis,
+ LazyL0StringFlowAnalysis
+ )
+}
+
+object LazyL0StringFlowAnalysis extends LazyStringFlowAnalysis {
+
+ override def init(p: SomeProject, ps: PropertyStore): InitializationData = L0InterpretationHandler(p)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/BinaryExprInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/BinaryExprInterpreter.scala
new file mode 100644
index 0000000000..75f0b279e9
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/BinaryExprInterpreter.scala
@@ -0,0 +1,56 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l0
+package interpretation
+
+import org.opalj.br.ComputationalTypeFloat
+import org.opalj.br.ComputationalTypeInt
+import org.opalj.br.fpcf.properties.string.StringTreeDynamicFloat
+import org.opalj.br.fpcf.properties.string.StringTreeDynamicInt
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * Interprets the given assignment statement containing a [[BinaryExpr]] by determining its return type and using the
+ * appropriate dynamic [[StringTreeNode]] in high soundness mode. In low soundness mode, no string value can be
+ * determined.
+ *
+ * @author Maximilian Rüsch
+ */
+case class BinaryExprInterpreter()(
+ implicit val highSoundness: Boolean
+) extends AssignmentBasedStringInterpreter {
+
+ override type E = BinaryExpr[V]
+
+ /**
+ * Currently, this implementation supports the interpretation of the following binary expressions:
+ *
+ * - [[ComputationalTypeInt]]
+ *
- [[ComputationalTypeFloat]]
+ *
+ * For all other expressions, [[StringFlowFunctionProperty.identity]] will be returned.
+ */
+ override def interpretExpr(target: PV, expr: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ // IMPROVE Use the underlying domain to retrieve the result of such expressions if possible in low soundness mode
+ computeFinalResult(expr.cTpe match {
+ case ComputationalTypeInt if highSoundness =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeDynamicInt)
+ case ComputationalTypeInt =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeNode.ub)
+ case ComputationalTypeFloat if highSoundness =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeDynamicFloat)
+ case ComputationalTypeFloat =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeNode.ub)
+ case _ => StringFlowFunctionProperty.identity
+ })
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/L0InterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/L0InterpretationHandler.scala
new file mode 100644
index 0000000000..b4cf9c74e2
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/L0InterpretationHandler.scala
@@ -0,0 +1,59 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l0
+package interpretation
+
+import org.opalj.br.analyses.SomeProject
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * @inheritdoc
+ *
+ * Interprets statements on a very basic level by only interpreting either constant or binary expressions and their
+ * resulting assignments.
+ *
+ * @author Maximilian Rüsch
+ */
+class L0InterpretationHandler(implicit override val project: SomeProject) extends InterpretationHandler {
+
+ override protected def processStatement(implicit
+ state: InterpretationState
+ ): Stmt[V] => ProperPropertyComputationResult = {
+ case stmt @ Assignment(_, _, expr: SimpleValueConst) =>
+ SimpleValueConstExprInterpreter.interpretExpr(stmt, expr)
+ case stmt @ Assignment(_, _, expr: BinaryExpr[V]) => BinaryExprInterpreter().interpretExpr(stmt, expr)
+
+ case ExprStmt(_, expr: VirtualFunctionCall[V]) => StringInterpreter.failure(expr.receiver.asVar)
+ case ExprStmt(_, expr: NonVirtualFunctionCall[V]) => StringInterpreter.failure(expr.receiver.asVar)
+
+ // Static function calls without return value usage are irrelevant
+ case ExprStmt(_, _: StaticFunctionCall[V]) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+
+ case vmc: VirtualMethodCall[V] => StringInterpreter.failure(vmc.receiver.asVar)
+ case nvmc: NonVirtualMethodCall[V] => StringInterpreter.failure(nvmc.receiver.asVar)
+
+ case Assignment(_, target, _) => StringInterpreter.failure(target)
+
+ case ReturnValue(pc, expr) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identityForVariableAt(
+ pc,
+ expr.asVar.toPersistentForm(state.tac.stmts)
+ ))
+
+ case _ =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+ }
+}
+
+object L0InterpretationHandler {
+
+ def apply(project: SomeProject): L0InterpretationHandler = new L0InterpretationHandler()(project)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/SimpleValueConstExprInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/SimpleValueConstExprInterpreter.scala
new file mode 100644
index 0000000000..84c888e956
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/SimpleValueConstExprInterpreter.scala
@@ -0,0 +1,44 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l0
+package interpretation
+
+import org.opalj.br.fpcf.properties.string.StringTreeConst
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * Interprets the given assignment statement containing a [[SimpleValueConst]] expression by determining the possible
+ * constant values from the given expression. The result is converted to a [[StringTreeConst]] and applied to the
+ * assignment target variable in the string flow function. If no applicable const is found, ID is returned for all
+ * variables.
+ *
+ * @author Maximilian Rüsch
+ */
+object SimpleValueConstExprInterpreter extends AssignmentBasedStringInterpreter {
+
+ override type E = SimpleValueConst
+
+ override def interpretExpr(target: PV, expr: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ computeFinalResult(expr match {
+ case ic: IntConst =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeConst(ic.value.toString))
+ case fc: FloatConst =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeConst(fc.value.toString))
+ case dc: DoubleConst =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeConst(dc.value.toString))
+ case lc: LongConst =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeConst(lc.value.toString))
+ case sc: StringConst =>
+ StringFlowFunctionProperty.constForVariableAt(state.pc, target, StringTreeConst(sc.value))
+ case _ => StringFlowFunctionProperty.identity
+ })
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/L1StringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/L1StringAnalysis.scala
new file mode 100644
index 0000000000..14b045d2d4
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/L1StringAnalysis.scala
@@ -0,0 +1,32 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.analyses.string.flowanalysis.LazyMethodStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.interpretation.LazyStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1InterpretationHandler
+
+/**
+ * @see [[L1InterpretationHandler]]
+ * @author Maximilian Rüsch
+ */
+object LazyL1StringAnalysis {
+
+ def allRequiredAnalyses: Seq[FPCFLazyAnalysisScheduler] = Seq(
+ LazyStringAnalysis,
+ LazyMethodStringFlowAnalysis,
+ LazyL1StringFlowAnalysis
+ )
+}
+
+object LazyL1StringFlowAnalysis extends LazyStringFlowAnalysis {
+
+ override def init(p: SomeProject, ps: PropertyStore): InitializationData = L1InterpretationHandler(p)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1FunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1FunctionCallInterpreter.scala
new file mode 100644
index 0000000000..daa2ffe0ad
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1FunctionCallInterpreter.scala
@@ -0,0 +1,177 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+package interpretation
+
+import org.opalj.br.Method
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.br.fpcf.properties.string.StringTreeOr
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.EUBP
+import org.opalj.fpcf.InterimResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.PropertyStore
+import org.opalj.fpcf.SomeEOptionP
+import org.opalj.fpcf.SomeEPS
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.StringFlowFunction
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment
+
+/**
+ * Base trait for all function call interpreters on L1. Provides support for multiple possible called methods as well as
+ * adding called methods and return dependees at runtime.
+ *
+ * @author Maximilian Rüsch
+ */
+trait L1FunctionCallInterpreter
+ extends AssignmentLikeBasedStringInterpreter
+ with ParameterEvaluatingStringInterpreter {
+
+ override type E <: FunctionCall[V]
+
+ implicit val ps: PropertyStore
+ implicit val highSoundness: Boolean
+
+ type CallState <: FunctionCallState
+
+ protected[this] class FunctionCallState(
+ val target: PV,
+ val parameters: Seq[PV],
+ var calleeMethods: Seq[Method] = Seq.empty,
+ var tacDependees: Map[Method, EOptionP[Method, TACAI]] = Map.empty,
+ var returnDependees: Map[Method, Seq[EOptionP[VariableDefinition, StringConstancyProperty]]] = Map.empty
+ ) {
+ var hasUnresolvableReturnValue: Map[Method, Boolean] = Map.empty.withDefaultValue(false)
+
+ def addCalledMethod(m: Method, tacDependee: EOptionP[Method, TACAI]): Unit = {
+ calleeMethods = calleeMethods :+ m
+ tacDependees = tacDependees.updated(m, tacDependee)
+ }
+
+ def updateReturnDependee(
+ method: Method,
+ newDependee: EOptionP[VariableDefinition, StringConstancyProperty]
+ ): Unit = {
+ returnDependees = returnDependees.updated(
+ method,
+ returnDependees(method).updated(
+ returnDependees(method).indexWhere(_.e == newDependee.e),
+ newDependee
+ )
+ )
+ }
+
+ def hasDependees: Boolean = {
+ tacDependees.values.exists(_.isRefinable) ||
+ returnDependees.values.flatten.exists(_.isRefinable)
+ }
+
+ def dependees: Iterable[SomeEOptionP] = {
+ tacDependees.values.filter(_.isRefinable) ++
+ returnDependees.values.flatten.filter(_.isRefinable)
+ }
+ }
+
+ protected def interpretArbitraryCallToFunctions(implicit
+ state: InterpretationState,
+ callState: CallState
+ ): ProperPropertyComputationResult = {
+ callState.calleeMethods.foreach { m =>
+ val tacEOptP = callState.tacDependees(m)
+ if (tacEOptP.hasUBP) {
+ val calleeTac = tacEOptP.ub.tac
+ if (calleeTac.isEmpty) {
+ // When we do not have a callee tac, we cannot infer arbitrary call return values at all
+ callState.hasUnresolvableReturnValue += m -> true
+ } else {
+ val returns = calleeTac.get.stmts.toIndexedSeq.filter(stmt => stmt.isInstanceOf[ReturnValue[V]])
+ callState.returnDependees += m -> returns.map { ret =>
+ val entity = VariableDefinition(
+ ret.pc,
+ ret.asInstanceOf[ReturnValue[V]].expr.asVar.toPersistentForm(calleeTac.get.stmts),
+ m
+ )
+ ps(entity, StringConstancyProperty.key)
+ }
+ }
+ }
+ }
+
+ tryComputeFinalResult
+ }
+
+ private def tryComputeFinalResult(
+ implicit
+ state: InterpretationState,
+ callState: CallState
+ ): ProperPropertyComputationResult = {
+ val pc = state.pc
+ val parameters = callState.parameters.zipWithIndex.map(x => (x._2, x._1)).toMap
+
+ val flowFunction: StringFlowFunction = (env: StringTreeEnvironment) =>
+ env.update(
+ pc,
+ callState.target,
+ StringTreeOr {
+ callState.calleeMethods.map { m =>
+ if (callState.hasUnresolvableReturnValue(m)) {
+ // We know we cannot resolve a definitive return value for this function
+ failureTree
+ } else if (callState.returnDependees.contains(m)) {
+ // We have some return dependees and can thus join their state
+ StringTreeOr(callState.returnDependees(m).map { rd =>
+ if (rd.hasUBP) {
+ rd.ub.tree.replaceParameters(parameters.map { kv => (kv._1, env(pc, kv._2)) })
+ } else StringTreeNode.ub
+ })
+ } else {
+ // Empty join -> Upper bound
+ StringTreeNode.ub
+ }
+ }
+ }
+ )
+
+ val newUB = StringFlowFunctionProperty(
+ callState.parameters.map(PDUWeb(pc, _)).toSet + PDUWeb(pc, callState.target),
+ flowFunction
+ )
+
+ if (callState.hasDependees) {
+ InterimResult.forUB(
+ InterpretationHandler.getEntity(state),
+ newUB,
+ callState.dependees.toSet,
+ continuation(state, callState)
+ )
+ } else {
+ computeFinalResult(newUB)
+ }
+ }
+
+ protected[this] def continuation(
+ state: InterpretationState,
+ callState: CallState
+ )(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case EUBP(m: Method, _: TACAI) =>
+ callState.tacDependees += m -> eps.asInstanceOf[EOptionP[Method, TACAI]]
+ interpretArbitraryCallToFunctions(state, callState)
+
+ case EUBP(_, _: StringConstancyProperty) =>
+ val contextEPS = eps.asInstanceOf[EOptionP[VariableDefinition, StringConstancyProperty]]
+ callState.updateReturnDependee(contextEPS.e.m, contextEPS)
+ tryComputeFinalResult(state, callState)
+
+ case _ => throw new IllegalArgumentException(s"Encountered unknown eps: $eps")
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1InterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1InterpretationHandler.scala
new file mode 100644
index 0000000000..27afbac4b3
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1InterpretationHandler.scala
@@ -0,0 +1,78 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+package interpretation
+
+import org.opalj.br.analyses.SomeProject
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.analyses.string.l0.interpretation.BinaryExprInterpreter
+import org.opalj.tac.fpcf.analyses.string.l0.interpretation.SimpleValueConstExprInterpreter
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * @inheritdoc
+ *
+ * Interprets statements similar to the [[org.opalj.tac.fpcf.analyses.string.l0.interpretation.L0InterpretationHandler]]
+ * but handles all sorts of function calls on top.
+ *
+ * @author Maximilian Rüsch
+ */
+class L1InterpretationHandler(implicit override val project: SomeProject) extends InterpretationHandler {
+
+ override protected def processStatement(implicit
+ state: InterpretationState
+ ): Stmt[V] => ProperPropertyComputationResult = {
+ case stmt @ Assignment(_, _, expr: SimpleValueConst) =>
+ SimpleValueConstExprInterpreter.interpretExpr(stmt, expr)
+ case stmt @ Assignment(_, _, expr: BinaryExpr[V]) => BinaryExprInterpreter().interpretExpr(stmt, expr)
+
+ // Currently unsupported
+ case Assignment(_, target, _: ArrayExpr[V]) => StringInterpreter.failure(target)
+ case Assignment(_, target, _: FieldRead[V]) => StringInterpreter.failure(target)
+
+ case Assignment(_, _, _: New) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+
+ case stmt @ Assignment(_, _, expr: VirtualFunctionCall[V]) =>
+ new L1VirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+ case stmt @ ExprStmt(_, expr: VirtualFunctionCall[V]) =>
+ new L1VirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+
+ case stmt @ Assignment(_, _, expr: NonVirtualFunctionCall[V]) =>
+ L1NonVirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+ case stmt @ ExprStmt(_, expr: NonVirtualFunctionCall[V]) =>
+ L1NonVirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+
+ case stmt @ Assignment(_, _, expr: StaticFunctionCall[V]) =>
+ L1StaticFunctionCallInterpreter().interpretExpr(stmt, expr)
+ // Static function calls without return value usage are irrelevant
+ case ExprStmt(_, _: StaticFunctionCall[V]) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+
+ case vmc: VirtualMethodCall[V] => L1VirtualMethodCallInterpreter().interpret(vmc)
+ case nvmc: NonVirtualMethodCall[V] => L1NonVirtualMethodCallInterpreter().interpret(nvmc)
+
+ case Assignment(_, target, _) =>
+ StringInterpreter.failure(target)
+
+ case ReturnValue(pc, expr) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identityForVariableAt(
+ pc,
+ expr.asVar.toPersistentForm(state.tac.stmts)
+ ))
+
+ case _ =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+ }
+}
+
+object L1InterpretationHandler {
+
+ def apply(project: SomeProject): L1InterpretationHandler = new L1InterpretationHandler()(project)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1NonVirtualFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1NonVirtualFunctionCallInterpreter.scala
new file mode 100644
index 0000000000..63893d7ff7
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1NonVirtualFunctionCallInterpreter.scala
@@ -0,0 +1,47 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+package interpretation
+
+import org.opalj.br.analyses.SomeProject
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.TACAI
+
+/**
+ * Processes [[NonVirtualFunctionCall]]s without a call graph.
+ *
+ * @author Maximilian Rüsch
+ */
+case class L1NonVirtualFunctionCallInterpreter()(
+ implicit val p: SomeProject,
+ implicit val ps: PropertyStore,
+ implicit val highSoundness: Boolean
+) extends AssignmentLikeBasedStringInterpreter
+ with L1FunctionCallInterpreter {
+
+ override type T = AssignmentLikeStmt[V]
+ override type E = NonVirtualFunctionCall[V]
+ override type CallState = FunctionCallState
+
+ override def interpretExpr(instr: T, expr: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ val target = expr.receiver.asVar.toPersistentForm(state.tac.stmts)
+ val calleeMethod = expr.resolveCallTarget(state.dm.definedMethod.classFile.thisType)
+ if (calleeMethod.isEmpty) {
+ return failure(target)
+ }
+
+ val m = calleeMethod.value
+ val params = getParametersForPC(state.pc).map(_.asVar.toPersistentForm(state.tac.stmts))
+ val callState = new FunctionCallState(target, params, Seq(m), Map((m, ps(m, TACAI.key))))
+
+ interpretArbitraryCallToFunctions(state, callState)
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1NonVirtualMethodCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1NonVirtualMethodCallInterpreter.scala
new file mode 100644
index 0000000000..5a908ebabc
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1NonVirtualMethodCallInterpreter.scala
@@ -0,0 +1,57 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+package interpretation
+
+import org.opalj.br.ObjectType
+import org.opalj.br.fpcf.properties.string.StringTreeEmptyConst
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment
+
+/**
+ * Processes [[NonVirtualMethodCall]]s without a call graph. Currently, only calls to `` of strings, string
+ * buffers and string builders are interpreted. For other calls, ID is returned.
+ *
+ * @author Maximilian Rüsch
+ */
+case class L1NonVirtualMethodCallInterpreter()(
+ implicit val highSoundness: Boolean
+) extends StringInterpreter {
+
+ override type T = NonVirtualMethodCall[V]
+
+ override def interpret(instr: T)(implicit state: InterpretationState): ProperPropertyComputationResult = {
+ instr.name match {
+ case ""
+ if instr.declaringClass.asReferenceType == ObjectType.StringBuffer ||
+ instr.declaringClass.asReferenceType == ObjectType.StringBuilder ||
+ instr.declaringClass.asReferenceType == ObjectType.String =>
+ interpretInit(instr)
+ case _ => computeFinalResult(StringFlowFunctionProperty.identity)
+ }
+ }
+
+ private def interpretInit(init: T)(implicit state: InterpretationState): ProperPropertyComputationResult = {
+ val pc = state.pc
+ val targetVar = init.receiver.asVar.toPersistentForm(state.tac.stmts)
+ init.params.size match {
+ case 0 =>
+ computeFinalResult(StringFlowFunctionProperty.constForVariableAt(pc, targetVar, StringTreeEmptyConst))
+ case 1 =>
+ val paramVar = init.params.head.asVar.toPersistentForm(state.tac.stmts)
+
+ computeFinalResult(
+ Set(PDUWeb(pc, targetVar), PDUWeb(pc, paramVar)),
+ (env: StringTreeEnvironment) => env.update(pc, targetVar, env(pc, paramVar))
+ )
+ case _ =>
+ failure(targetVar)
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1StaticFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1StaticFunctionCallInterpreter.scala
new file mode 100644
index 0000000000..34398108b8
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1StaticFunctionCallInterpreter.scala
@@ -0,0 +1,108 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+package interpretation
+
+import org.opalj.br.ObjectType
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.properties.string.StringTreeConst
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.StringFlowFunction
+import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment
+
+/**
+ * Interprets some specific static calls in the context of their method as well as arbitrary static calls without a call
+ * graph.
+ *
+ * @see [[L1ArbitraryStaticFunctionCallInterpreter]], [[L1StringValueOfFunctionCallInterpreter]],
+ * [[L1SystemPropertiesInterpreter]]
+ *
+ * @author Maximilian Rüsch
+ */
+case class L1StaticFunctionCallInterpreter()(
+ implicit
+ override val p: SomeProject,
+ override val ps: PropertyStore,
+ override val project: SomeProject,
+ val highSoundness: Boolean
+) extends AssignmentBasedStringInterpreter
+ with L1ArbitraryStaticFunctionCallInterpreter
+ with L1StringValueOfFunctionCallInterpreter
+ with L1SystemPropertiesInterpreter {
+
+ override type E = StaticFunctionCall[V]
+
+ override def interpretExpr(target: PV, call: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ call.name match {
+ case "getProperty" if call.declaringClass == ObjectType.System => interpretGetSystemPropertiesCall(target)
+ case "valueOf" if call.declaringClass == ObjectType.String => processStringValueOf(target, call)
+ case _
+ if call.descriptor.returnType == ObjectType.String ||
+ call.descriptor.returnType == ObjectType.Object =>
+ interpretArbitraryCall(target, call)
+ case _ => failure(target)
+ }
+ }
+}
+
+private[string] trait L1ArbitraryStaticFunctionCallInterpreter
+ extends AssignmentBasedStringInterpreter
+ with L1FunctionCallInterpreter {
+
+ implicit val p: SomeProject
+
+ override type E <: StaticFunctionCall[V]
+ override type CallState = FunctionCallState
+
+ def interpretArbitraryCall(target: PV, call: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ val calleeMethod = call.resolveCallTarget(state.dm.definedMethod.classFile.thisType)
+ if (calleeMethod.isEmpty) {
+ return failure(target)
+ }
+
+ val m = calleeMethod.value
+ val params = getParametersForPC(state.pc).map(_.asVar.toPersistentForm(state.tac.stmts))
+ val callState = new FunctionCallState(target, params, Seq(m), Map((m, ps(m, TACAI.key))))
+
+ interpretArbitraryCallToFunctions(state, callState)
+ }
+}
+
+private[string] trait L1StringValueOfFunctionCallInterpreter extends AssignmentBasedStringInterpreter {
+
+ override type E <: StaticFunctionCall[V]
+
+ def processStringValueOf(target: PV, call: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ val pc = state.pc
+ val pp = call.params.head.asVar.toPersistentForm(state.tac.stmts)
+
+ val flowFunction: StringFlowFunction = if (call.descriptor.parameterType(0).toJava == "char") {
+ (env: StringTreeEnvironment) =>
+ {
+ env(pc, pp) match {
+ case const: StringTreeConst if const.isIntConst =>
+ env.update(pc, target, StringTreeConst(const.string.toInt.toChar.toString))
+ case tree =>
+ env.update(pc, target, tree)
+ }
+ }
+ } else {
+ (env: StringTreeEnvironment) => env.update(pc, target, env(pc, pp))
+ }
+
+ computeFinalResult(Set(PDUWeb(pc, pp), PDUWeb(pc, target)), flowFunction)
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1SystemPropertiesInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1SystemPropertiesInterpreter.scala
new file mode 100644
index 0000000000..3f6f10c653
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1SystemPropertiesInterpreter.scala
@@ -0,0 +1,83 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+package interpretation
+
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.properties.SystemProperties
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.br.fpcf.properties.string.StringTreeOr
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.InterimResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.PropertyStore
+import org.opalj.fpcf.SomeEPS
+import org.opalj.fpcf.UBP
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * Processes assignments of system property references to variables by using the [[SystemProperties]] FPCF property.
+ *
+ * @author Maximilian Rüsch
+ */
+private[string] trait L1SystemPropertiesInterpreter extends StringInterpreter {
+
+ implicit val ps: PropertyStore
+ implicit val project: SomeProject
+
+ private case class SystemPropertiesDepender(target: PV, var dependee: EOptionP[SomeProject, SystemProperties])
+
+ protected def interpretGetSystemPropertiesCall(target: PV)(
+ implicit state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ val depender = SystemPropertiesDepender(target, ps(project, SystemProperties.key))
+
+ if (depender.dependee.isEPK) {
+ InterimResult.forUB(
+ InterpretationHandler.getEntity(state),
+ StringFlowFunctionProperty.constForVariableAt(
+ state.pc,
+ depender.target,
+ StringTreeNode.ub
+ ),
+ Set(depender.dependee),
+ continuation(state, depender)
+ )
+ } else {
+ continuation(state, depender)(depender.dependee.asInstanceOf[SomeEPS])
+ }
+ }
+
+ private def continuation(
+ state: InterpretationState,
+ depender: SystemPropertiesDepender
+ )(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case UBP(ub: SystemProperties) =>
+ depender.dependee = eps.asInstanceOf[EOptionP[SomeProject, SystemProperties]]
+ val newUB = StringFlowFunctionProperty.constForVariableAt(
+ state.pc,
+ depender.target,
+ StringTreeOr(ub.values.toSeq)
+ )
+ if (depender.dependee.isRefinable) {
+ InterimResult.forUB(
+ InterpretationHandler.getEntity(state),
+ newUB,
+ Set(depender.dependee),
+ continuation(state, depender)
+ )
+ } else {
+ computeFinalResult(newUB)(state)
+ }
+
+ case _ => throw new IllegalArgumentException(s"Encountered unknown eps: $eps")
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1VirtualFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1VirtualFunctionCallInterpreter.scala
new file mode 100644
index 0000000000..0028340b9e
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1VirtualFunctionCallInterpreter.scala
@@ -0,0 +1,232 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+package interpretation
+
+import org.opalj.br.ComputationalTypeDouble
+import org.opalj.br.ComputationalTypeFloat
+import org.opalj.br.ComputationalTypeInt
+import org.opalj.br.DoubleType
+import org.opalj.br.FloatType
+import org.opalj.br.IntLikeType
+import org.opalj.br.ObjectType
+import org.opalj.br.fpcf.properties.string.StringConstancyLevel
+import org.opalj.br.fpcf.properties.string.StringTreeConcat
+import org.opalj.br.fpcf.properties.string.StringTreeConst
+import org.opalj.br.fpcf.properties.string.StringTreeDynamicFloat
+import org.opalj.br.fpcf.properties.string.StringTreeDynamicInt
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment
+import org.opalj.value.TheIntegerValue
+
+/**
+ * Processes [[VirtualFunctionCall]]s without a call graph. Some string operations such as `append`, `toString` or
+ * `substring` are either fully interpreted or approximated.
+ *
+ * @note Due to a missing call graph, arbitrary (i.e. not otherwise interpreted) virtual function calls will not be
+ * interpreted.
+ *
+ * @author Maximilian Rüsch
+ */
+class L1VirtualFunctionCallInterpreter(
+ implicit val highSoundness: Boolean
+) extends AssignmentLikeBasedStringInterpreter
+ with L1ArbitraryVirtualFunctionCallInterpreter
+ with L1AppendCallInterpreter
+ with L1SubstringCallInterpreter {
+
+ override type T = AssignmentLikeStmt[V]
+ override type E = VirtualFunctionCall[V]
+
+ override def interpretExpr(instr: T, call: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ val at = Option.unless(!instr.isAssignment)(instr.asAssignment.targetVar.asVar.toPersistentForm(state.tac.stmts))
+ val pt = call.receiver.asVar.toPersistentForm(state.tac.stmts)
+
+ call.name match {
+ case "append" => interpretAppendCall(at, pt, call)
+ case "toString" => interpretToStringCall(at, pt)
+ case "replace" => interpretReplaceCall(pt)
+ case "substring" if call.descriptor.returnType == ObjectType.String =>
+ interpretSubstringCall(at, pt, call)
+ case _ =>
+ call.descriptor.returnType match {
+ case _: IntLikeType if at.isDefined =>
+ computeFinalResult(StringFlowFunctionProperty.constForVariableAt(
+ state.pc,
+ at.get,
+ if (highSoundness) StringTreeDynamicInt
+ else StringTreeNode.ub
+ ))
+ case FloatType | DoubleType if at.isDefined =>
+ computeFinalResult(StringFlowFunctionProperty.constForVariableAt(
+ state.pc,
+ at.get,
+ if (highSoundness) StringTreeDynamicFloat
+ else StringTreeNode.ub
+ ))
+ case _ if at.isDefined =>
+ interpretArbitraryCall(at.get, call)
+ case _ =>
+ computeFinalResult(StringFlowFunctionProperty.identity)
+ }
+ }
+ }
+
+ private def interpretToStringCall(at: Option[PV], pt: PV)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ if (at.isDefined) {
+ computeFinalResult(
+ Set(PDUWeb(state.pc, at.get), PDUWeb(state.pc, pt)),
+ (env: StringTreeEnvironment) => env.update(state.pc, at.get, env(state.pc, pt))
+ )
+ } else {
+ computeFinalResult(StringFlowFunctionProperty.identity)
+ }
+ }
+
+ /**
+ * Processes calls to [[StringBuilder#replace]] or [[StringBuffer#replace]].
+ */
+ private def interpretReplaceCall(target: PV)(implicit state: InterpretationState): ProperPropertyComputationResult = {
+ // Improve: Support fluent API by returning combined web for both assignment target and call target
+ failure(target)
+ }
+}
+
+private[string] trait L1ArbitraryVirtualFunctionCallInterpreter extends AssignmentLikeBasedStringInterpreter {
+
+ implicit val highSoundness: Boolean
+
+ protected def interpretArbitraryCall(target: PV, call: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = failure(target)
+}
+
+/**
+ * Interprets calls to [[StringBuilder#append]] or [[StringBuffer#append]].
+ *
+ * @author Maximilian Rüsch
+ */
+private[string] trait L1AppendCallInterpreter extends AssignmentLikeBasedStringInterpreter {
+
+ val highSoundness: Boolean
+
+ override type E = VirtualFunctionCall[V]
+
+ def interpretAppendCall(at: Option[PV], pt: PV, call: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ // .head because we want to evaluate only the first argument of append
+ val paramVar = call.params.head.asVar.toPersistentForm(state.tac.stmts)
+
+ val ptWeb = PDUWeb(state.pc, pt)
+ val combinedWeb = if (at.isDefined) ptWeb.combine(PDUWeb(state.pc, at.get)) else ptWeb
+
+ computeFinalResult(
+ Set(PDUWeb(state.pc, paramVar), combinedWeb),
+ (env: StringTreeEnvironment) => {
+ val valueState = env(state.pc, paramVar)
+
+ val transformedValueState = paramVar.value.computationalType match {
+ case ComputationalTypeInt =>
+ if (call.descriptor.parameterType(0).isCharType && valueState.isInstanceOf[StringTreeConst]) {
+ StringTreeConst(valueState.asInstanceOf[StringTreeConst].string.toInt.toChar.toString)
+ } else {
+ valueState
+ }
+ case ComputationalTypeFloat | ComputationalTypeDouble =>
+ if (valueState.constancyLevel == StringConstancyLevel.Constant) {
+ valueState
+ } else {
+ if (highSoundness) StringTreeDynamicFloat
+ else StringTreeNode.ub
+ }
+ case _ =>
+ valueState
+ }
+
+ env.update(combinedWeb, StringTreeConcat.fromNodes(env(state.pc, pt), transformedValueState))
+ }
+ )
+ }
+}
+
+/**
+ * Interprets calls to [[String#substring]].
+ *
+ * @author Maximilian Rüsch
+ */
+private[string] trait L1SubstringCallInterpreter extends AssignmentLikeBasedStringInterpreter {
+
+ override type E <: VirtualFunctionCall[V]
+
+ implicit val highSoundness: Boolean
+
+ def interpretSubstringCall(at: Option[PV], pt: PV, call: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ if (at.isEmpty) {
+ return computeFinalResult(StringFlowFunctionProperty.identity);
+ }
+
+ val parameterCount = call.params.size
+ parameterCount match {
+ case 1 =>
+ call.params.head.asVar.value match {
+ case TheIntegerValue(intVal) =>
+ computeFinalResult(
+ Set(PDUWeb(state.pc, pt), PDUWeb(state.pc, at.get)),
+ (env: StringTreeEnvironment) => {
+ env(state.pc, pt) match {
+ case StringTreeConst(string) if intVal <= string.length =>
+ env.update(state.pc, at.get, StringTreeConst(string.substring(intVal)))
+ case _ =>
+ env.update(state.pc, at.get, failureTree)
+ }
+ }
+ )
+ case _ =>
+ computeFinalResult(StringFlowFunctionProperty.constForVariableAt(state.pc, at.get, failureTree))
+ }
+
+ case 2 =>
+ (call.params.head.asVar.value, call.params(1).asVar.value) match {
+ case (TheIntegerValue(firstIntVal), TheIntegerValue(secondIntVal)) =>
+ computeFinalResult(
+ Set(PDUWeb(state.pc, pt), PDUWeb(state.pc, at.get)),
+ (env: StringTreeEnvironment) => {
+ env(state.pc, pt) match {
+ case StringTreeConst(string)
+ if firstIntVal <= string.length
+ && secondIntVal <= string.length
+ && firstIntVal <= secondIntVal =>
+ env.update(
+ state.pc,
+ at.get,
+ StringTreeConst(string.substring(firstIntVal, secondIntVal))
+ )
+ case _ =>
+ env.update(state.pc, at.get, failureTree)
+ }
+ }
+ )
+ case _ =>
+ computeFinalResult(StringFlowFunctionProperty.constForVariableAt(state.pc, at.get, failureTree))
+ }
+
+ case _ => throw new IllegalStateException(
+ s"Unexpected parameter count for ${call.descriptor.toJava}. Expected one or two, got $parameterCount"
+ )
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1VirtualMethodCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1VirtualMethodCallInterpreter.scala
new file mode 100644
index 0000000000..cbcc7aa6a5
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l1/interpretation/L1VirtualMethodCallInterpreter.scala
@@ -0,0 +1,69 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l1
+package interpretation
+
+import org.opalj.br.fpcf.properties.string.StringTreeConst
+import org.opalj.br.fpcf.properties.string.StringTreeEmptyConst
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment
+import org.opalj.value.TheIntegerValue
+
+/**
+ * Processes [[VirtualMethodCall]]s without a call graph. Currently, only calls to `setLength` of string buffers and
+ * string builders are interpreted. For other calls, ID is returned.
+ *
+ * @author Maximilian Rüsch
+ */
+case class L1VirtualMethodCallInterpreter()(
+ implicit val highSoundness: Boolean
+) extends StringInterpreter {
+
+ override type T = VirtualMethodCall[V]
+
+ override def interpret(call: T)(implicit state: InterpretationState): ProperPropertyComputationResult = {
+ val pReceiver = call.receiver.asVar.toPersistentForm(state.tac.stmts)
+
+ call.name match {
+ case "setLength" =>
+ call.params.head.asVar.value match {
+ case TheIntegerValue(intVal) if intVal == 0 =>
+ computeFinalResult(StringFlowFunctionProperty.constForVariableAt(
+ state.pc,
+ pReceiver,
+ StringTreeEmptyConst
+ ))
+
+ case TheIntegerValue(intVal) =>
+ computeFinalResult(
+ PDUWeb(state.pc, pReceiver),
+ (env: StringTreeEnvironment) => {
+ env(state.pc, pReceiver) match {
+ case StringTreeConst(string) =>
+ val sb = new StringBuilder(string)
+ sb.setLength(intVal)
+ env.update(state.pc, pReceiver, StringTreeConst(sb.toString()))
+ case _ =>
+ env.update(
+ state.pc,
+ pReceiver,
+ failureTree
+ )
+ }
+ }
+ )
+ case _ =>
+ computeFinalResult(StringFlowFunctionProperty.ub(state.pc, pReceiver))
+ }
+
+ case _ =>
+ computeFinalResult(StringFlowFunctionProperty.identity)
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/L2StringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/L2StringAnalysis.scala
new file mode 100644
index 0000000000..ffc1512344
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/L2StringAnalysis.scala
@@ -0,0 +1,46 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l2
+
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
+import org.opalj.br.fpcf.properties.SystemProperties
+import org.opalj.br.fpcf.properties.cg.Callees
+import org.opalj.br.fpcf.properties.fieldaccess.FieldWriteAccessInformation
+import org.opalj.fpcf.PropertyBounds
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.analyses.string.flowanalysis.LazyMethodStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.interpretation.LazyStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l2.interpretation.L2InterpretationHandler
+
+/**
+ * @see [[L2InterpretationHandler]]
+ * @author Maximilian Rüsch
+ */
+object LazyL2StringAnalysis {
+
+ def allRequiredAnalyses: Seq[FPCFLazyAnalysisScheduler] = Seq(
+ LazyStringAnalysis,
+ LazyMethodStringFlowAnalysis,
+ LazyL2StringFlowAnalysis
+ )
+}
+
+object LazyL2StringFlowAnalysis extends LazyStringFlowAnalysis {
+
+ override final def uses: Set[PropertyBounds] = super.uses ++ PropertyBounds.ubs(
+ Callees,
+ FieldWriteAccessInformation,
+ SystemProperties
+ )
+
+ override final def init(p: SomeProject, ps: PropertyStore): InitializationData = L2InterpretationHandler(p)
+
+ override def requiredProjectInformation: ProjectInformationKeys = super.requiredProjectInformation ++
+ L2InterpretationHandler.requiredProjectInformation
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/interpretation/L2InterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/interpretation/L2InterpretationHandler.scala
new file mode 100644
index 0000000000..3b6f877d46
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/interpretation/L2InterpretationHandler.scala
@@ -0,0 +1,101 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l2
+package interpretation
+
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.ContextProviderKey
+import org.opalj.br.fpcf.analyses.ContextProvider
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.analyses.string.l0.interpretation.BinaryExprInterpreter
+import org.opalj.tac.fpcf.analyses.string.l0.interpretation.SimpleValueConstExprInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1NonVirtualFunctionCallInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1NonVirtualMethodCallInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1StaticFunctionCallInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1VirtualMethodCallInterpreter
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * @inheritdoc
+ *
+ * Interprets statements similar to [[org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1InterpretationHandler]] but
+ * handles virtual function calls using the call graph.
+ *
+ * @note This level can be expanded to handle all function calls via the call graph, not just virtual ones.
+ *
+ * @author Maximilian Rüsch
+ */
+class L2InterpretationHandler(implicit override val project: SomeProject) extends InterpretationHandler {
+
+ implicit val contextProvider: ContextProvider = p.get(ContextProviderKey)
+
+ override protected def processStatement(implicit
+ state: InterpretationState
+ ): Stmt[V] => ProperPropertyComputationResult = {
+ case stmt @ Assignment(_, _, expr: SimpleValueConst) =>
+ SimpleValueConstExprInterpreter.interpretExpr(stmt, expr)
+
+ // Currently unsupported
+ case Assignment(_, target, _: ArrayExpr[V]) => StringInterpreter.failure(target)
+ case Assignment(_, target, _: FieldRead[V]) => StringInterpreter.failure(target)
+
+ case stmt: FieldWriteAccessStmt[V] =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identityForVariableAt(
+ stmt.pc,
+ stmt.value.asVar.toPersistentForm(state.tac.stmts)
+ ))
+
+ case stmt @ Assignment(_, _, expr: VirtualFunctionCall[V]) =>
+ new L2VirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+ case stmt @ ExprStmt(_, expr: VirtualFunctionCall[V]) =>
+ new L2VirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+
+ // IMPROVE add call-graph based interpreters for other call types than virtual function calls to L2
+ case stmt @ Assignment(_, _, expr: NonVirtualFunctionCall[V]) =>
+ L1NonVirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+ case stmt @ ExprStmt(_, expr: NonVirtualFunctionCall[V]) =>
+ L1NonVirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+
+ case stmt @ Assignment(_, _, expr: StaticFunctionCall[V]) =>
+ L1StaticFunctionCallInterpreter().interpretExpr(stmt, expr)
+ // Static function calls without return value usage are irrelevant
+ case ExprStmt(_, _: StaticFunctionCall[V]) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+
+ case stmt @ Assignment(_, _, expr: BinaryExpr[V]) => BinaryExprInterpreter().interpretExpr(stmt, expr)
+
+ case vmc: VirtualMethodCall[V] =>
+ L1VirtualMethodCallInterpreter().interpret(vmc)
+ case nvmc: NonVirtualMethodCall[V] =>
+ L1NonVirtualMethodCallInterpreter().interpret(nvmc)
+
+ case Assignment(_, _, _: New) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+
+ case Assignment(_, target, _) =>
+ StringInterpreter.failure(target)
+
+ case ReturnValue(pc, expr) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identityForVariableAt(
+ pc,
+ expr.asVar.toPersistentForm(state.tac.stmts)
+ ))
+
+ case _ =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+ }
+}
+
+object L2InterpretationHandler {
+
+ def requiredProjectInformation: ProjectInformationKeys = Seq(ContextProviderKey)
+
+ def apply(project: SomeProject): L2InterpretationHandler = new L2InterpretationHandler()(project)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/interpretation/L2VirtualFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/interpretation/L2VirtualFunctionCallInterpreter.scala
new file mode 100644
index 0000000000..cbe37895a9
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l2/interpretation/L2VirtualFunctionCallInterpreter.scala
@@ -0,0 +1,132 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l2
+package interpretation
+
+import org.opalj.br.DefinedMethod
+import org.opalj.br.ObjectType
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.analyses.ContextProvider
+import org.opalj.br.fpcf.properties.Context
+import org.opalj.br.fpcf.properties.cg.Callees
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.InterimResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.PropertyStore
+import org.opalj.fpcf.SomeEOptionP
+import org.opalj.fpcf.SomeEPS
+import org.opalj.fpcf.UBP
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1FunctionCallInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1SystemPropertiesInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1VirtualFunctionCallInterpreter
+import org.opalj.tac.fpcf.properties.TACAI
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * Processes [[VirtualFunctionCall]]s similar to the [[L1VirtualFunctionCallInterpreter]] but handles arbitrary calls
+ * with a call graph.
+ *
+ * @author Maximilian Rüsch
+ */
+class L2VirtualFunctionCallInterpreter(
+ implicit val ps: PropertyStore,
+ implicit val contextProvider: ContextProvider,
+ implicit val project: SomeProject,
+ override implicit val highSoundness: Boolean
+) extends L1VirtualFunctionCallInterpreter
+ with StringInterpreter
+ with L1SystemPropertiesInterpreter
+ with L2ArbitraryVirtualFunctionCallInterpreter {
+
+ override type E = VirtualFunctionCall[V]
+
+ override protected def interpretArbitraryCall(target: PV, call: E)(
+ implicit state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ if (call.name == "getProperty" && call.declaringClass == ObjectType("java/util/Properties")) {
+ interpretGetSystemPropertiesCall(target)
+ } else {
+ interpretArbitraryCallWithCallees(target)
+ }
+ }
+}
+
+private[string] trait L2ArbitraryVirtualFunctionCallInterpreter extends L1FunctionCallInterpreter {
+
+ implicit val ps: PropertyStore
+ implicit val contextProvider: ContextProvider
+
+ override type CallState = CalleeDepender
+
+ protected[this] case class CalleeDepender(
+ override val target: PV,
+ override val parameters: Seq[PV],
+ methodContext: Context,
+ var calleeDependee: EOptionP[DefinedMethod, Callees],
+ var seenDirectCallees: Int = 0,
+ var seenIndirectCallees: Int = 0
+ ) extends FunctionCallState(target, parameters) {
+
+ override def hasDependees: Boolean = calleeDependee.isRefinable || super.hasDependees
+
+ override def dependees: Iterable[SomeEOptionP] = super.dependees ++ Seq(calleeDependee).filter(_.isRefinable)
+ }
+
+ protected def interpretArbitraryCallWithCallees(target: PV)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ val params = getParametersForPC(state.pc).map(_.asVar.toPersistentForm(state.tac.stmts))
+ // IMPROVE pass the actual method context through the entity - needs to be differentiated from "upward" entities
+ val depender = CalleeDepender(target, params, contextProvider.newContext(state.dm), ps(state.dm, Callees.key))
+
+ if (depender.calleeDependee.isEPK) {
+ InterimResult.forUB(
+ InterpretationHandler.getEntity(state),
+ StringFlowFunctionProperty.ub(state.pc, target),
+ Set(depender.calleeDependee),
+ continuation(state, depender)
+ )
+ } else {
+ continuation(state, depender)(depender.calleeDependee.asInstanceOf[SomeEPS])
+ }
+ }
+
+ override protected[this] def continuation(
+ state: InterpretationState,
+ callState: CallState
+ )(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case UBP(c: Callees) =>
+ val newCallees = c.directCallees(callState.methodContext, state.pc).drop(callState.seenDirectCallees) ++
+ c.indirectCallees(callState.methodContext, state.pc).drop(callState.seenIndirectCallees)
+
+ // IMPROVE add some uncertainty element if methods with unknown body exist
+ val newMethods = newCallees
+ .filter(_.method.hasSingleDefinedMethod)
+ .map(_.method.definedMethod)
+ .filterNot(callState.calleeMethods.contains)
+ .distinct.toList.sortBy(_.classFile.fqn)
+
+ callState.calleeDependee = eps.asInstanceOf[EOptionP[DefinedMethod, Callees]]
+ if (newMethods.isEmpty && callState.calleeMethods.isEmpty && eps.isFinal) {
+ failure(callState.target)(state, highSoundness)
+ } else {
+ for {
+ method <- newMethods
+ } {
+ callState.addCalledMethod(method, ps(method, TACAI.key))
+ }
+
+ interpretArbitraryCallToFunctions(state, callState)
+ }
+
+ case _ => super.continuation(state, callState)(eps)
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/L3StringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/L3StringAnalysis.scala
new file mode 100644
index 0000000000..9d524357d6
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/L3StringAnalysis.scala
@@ -0,0 +1,46 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l3
+
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
+import org.opalj.br.fpcf.properties.SystemProperties
+import org.opalj.br.fpcf.properties.cg.Callees
+import org.opalj.br.fpcf.properties.fieldaccess.FieldWriteAccessInformation
+import org.opalj.fpcf.PropertyBounds
+import org.opalj.fpcf.PropertyStore
+import org.opalj.tac.fpcf.analyses.string.flowanalysis.LazyMethodStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.interpretation.LazyStringFlowAnalysis
+import org.opalj.tac.fpcf.analyses.string.l3.interpretation.L3InterpretationHandler
+
+/**
+ * @see [[L3InterpretationHandler]]
+ * @author Maximilian Rüsch
+ */
+object LazyL3StringAnalysis {
+
+ def allRequiredAnalyses: Seq[FPCFLazyAnalysisScheduler] = Seq(
+ LazyStringAnalysis,
+ LazyMethodStringFlowAnalysis,
+ LazyL3StringFlowAnalysis
+ )
+}
+
+object LazyL3StringFlowAnalysis extends LazyStringFlowAnalysis {
+
+ override final def uses: Set[PropertyBounds] = super.uses ++ PropertyBounds.ubs(
+ Callees,
+ FieldWriteAccessInformation,
+ SystemProperties
+ )
+
+ override final def init(p: SomeProject, ps: PropertyStore): InitializationData = L3InterpretationHandler(p)
+
+ override def requiredProjectInformation: ProjectInformationKeys = super.requiredProjectInformation ++
+ L3InterpretationHandler.requiredProjectInformation
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/interpretation/L3FieldReadInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/interpretation/L3FieldReadInterpreter.scala
new file mode 100644
index 0000000000..fe89cee64c
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/interpretation/L3FieldReadInterpreter.scala
@@ -0,0 +1,219 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l3
+package interpretation
+
+import scala.collection.mutable.ListBuffer
+
+import org.opalj.br.DeclaredField
+import org.opalj.br.FieldType
+import org.opalj.br.ObjectType
+import org.opalj.br.PUVar
+import org.opalj.br.analyses.DeclaredFields
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.analyses.ContextProvider
+import org.opalj.br.fpcf.properties.fieldaccess.FieldWriteAccessInformation
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.br.fpcf.properties.string.StringTreeNull
+import org.opalj.br.fpcf.properties.string.StringTreeOr
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.InterimResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.PropertyStore
+import org.opalj.fpcf.SomeEOptionP
+import org.opalj.fpcf.SomeEPS
+import org.opalj.fpcf.UBP
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * Interprets direct reads to fields (see [[FieldRead]]) by analyzing the write accesses to these fields via the
+ * [[FieldWriteAccessInformation]] and the possible string values passed to these write accesses.
+ *
+ * @author Maximilian Rüsch
+ */
+class L3FieldReadInterpreter(
+ implicit val ps: PropertyStore,
+ implicit val project: SomeProject,
+ implicit val declaredFields: DeclaredFields,
+ implicit val contextProvider: ContextProvider,
+ implicit val highSoundness: Boolean
+) extends AssignmentBasedStringInterpreter {
+
+ override type E = FieldRead[V]
+
+ private case class FieldReadState(
+ target: PV,
+ var fieldAccessDependee: EOptionP[DeclaredField, FieldWriteAccessInformation],
+ var seenDirectFieldAccesses: Int = 0,
+ var seenIndirectFieldAccesses: Int = 0,
+ var hasWriteInSameMethod: Boolean = false,
+ var hasInit: Boolean = false,
+ var hasUnresolvableAccess: Boolean = false,
+ var accessDependees: Seq[EOptionP[VariableDefinition, StringConstancyProperty]] = Seq.empty,
+ previousResults: ListBuffer[StringTreeNode] = ListBuffer.empty
+ ) {
+
+ def updateAccessDependee(newDependee: EOptionP[VariableDefinition, StringConstancyProperty]): Unit = {
+ accessDependees = accessDependees.updated(
+ accessDependees.indexWhere(_.e == newDependee.e),
+ newDependee
+ )
+ }
+
+ def hasDependees: Boolean = fieldAccessDependee.isRefinable || accessDependees.exists(_.isRefinable)
+
+ def dependees: Iterable[SomeEOptionP] = {
+ val dependees = accessDependees.filter(_.isRefinable)
+
+ if (fieldAccessDependee.isRefinable) fieldAccessDependee +: dependees
+ else dependees
+ }
+ }
+
+ override def interpretExpr(target: PV, fieldRead: E)(implicit
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ if (!L3FieldReadInterpreter.isSupportedType(fieldRead.declaredFieldType)) {
+ return failure(target)
+ }
+
+ val field = declaredFields(fieldRead.declaringClass, fieldRead.name, fieldRead.declaredFieldType)
+ val fieldAccessEOptP = ps(field, FieldWriteAccessInformation.key)
+
+ implicit val accessState: FieldReadState = FieldReadState(target, fieldAccessEOptP)
+ if (fieldAccessEOptP.hasUBP) {
+ handleFieldAccessInformation(fieldAccessEOptP.ub)
+ } else {
+ InterimResult.forUB(
+ InterpretationHandler.getEntity,
+ StringFlowFunctionProperty.ub(state.pc, target),
+ accessState.dependees.toSet,
+ continuation(accessState, state)
+ )
+ }
+ }
+
+ private def handleFieldAccessInformation(accessInformation: FieldWriteAccessInformation)(
+ implicit
+ accessState: FieldReadState,
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ if (accessState.fieldAccessDependee.isFinal && accessInformation.accesses.isEmpty) {
+ // No methods which write the field were found => Field could either be null or any value
+ return computeFinalResult(StringFlowFunctionProperty.constForVariableAt(
+ state.pc,
+ accessState.target,
+ StringTreeOr.fromNodes(failureTree, StringTreeNull)
+ ))
+ }
+
+ accessInformation.getNewestAccesses(
+ accessInformation.numDirectAccesses - accessState.seenDirectFieldAccesses,
+ accessInformation.numIndirectAccesses - accessState.seenIndirectFieldAccesses
+ ).foreach {
+ case (contextId, pc, _, parameter) =>
+ val method = contextProvider.contextFromId(contextId).method.definedMethod
+
+ if (method == state.dm.definedMethod) {
+ accessState.hasWriteInSameMethod = true
+ }
+
+ if (method.name == "" || method.name == "") {
+ accessState.hasInit = true
+ }
+
+ if (parameter.isEmpty) {
+ // Field parameter information is not available
+ accessState.hasUnresolvableAccess = true
+ } else {
+ // IMPROVE use variable contexts here to support field writes based on method parameters in other
+ // methods. Requires a context to exist for variable definitions as well
+ val entity = VariableDefinition(pc, PUVar(parameter.get._1, parameter.get._2), method)
+ accessState.accessDependees = accessState.accessDependees :+ ps(entity, StringConstancyProperty.key)
+ }
+ }
+
+ accessState.seenDirectFieldAccesses = accessInformation.numDirectAccesses
+ accessState.seenIndirectFieldAccesses = accessInformation.numIndirectAccesses
+
+ tryComputeFinalResult
+ }
+
+ private def tryComputeFinalResult(implicit
+ accessState: FieldReadState,
+ state: InterpretationState
+ ): ProperPropertyComputationResult = {
+ if (accessState.hasWriteInSameMethod && highSoundness) {
+ // We cannot handle writes to a field that is read in the same method at the moment as the flow functions do
+ // not capture field state. This can be improved upon in the future.
+ computeFinalResult(StringFlowFunctionProperty.lb(state.pc, accessState.target))
+ } else {
+ var trees = accessState.accessDependees.map { ad =>
+ if (ad.hasUBP) {
+ val tree = ad.ub.tree
+ if (tree.parameterIndices.nonEmpty) {
+ // We cannot handle write values that contain parameter indices since resolving the parameters
+ // requires context and this interpreter is present in multiple contexts.
+ tree.replaceParameters(tree.parameterIndices.map((_, failureTree)).toMap)
+ } else
+ tree
+ } else StringTreeNode.ub
+ }
+ // No init is present => append a `null` element to indicate that the field might be null; this behavior
+ // could be refined by only setting the null element if no statement is guaranteed to be executed prior
+ // to the field read
+ if (accessState.fieldAccessDependee.isFinal && !accessState.hasInit) {
+ trees = trees :+ StringTreeNull
+ }
+
+ if (accessState.hasUnresolvableAccess && highSoundness) {
+ trees = trees :+ StringTreeNode.lb
+ }
+
+ val newUB = StringFlowFunctionProperty.constForVariableAt(state.pc, accessState.target, StringTreeOr(trees))
+ if (accessState.hasDependees) {
+ InterimResult.forUB(
+ InterpretationHandler.getEntity,
+ newUB,
+ accessState.dependees.toSet,
+ continuation(accessState, state)
+ )
+ } else {
+ computeFinalResult(newUB)
+ }
+ }
+ }
+
+ private def continuation(
+ accessState: FieldReadState,
+ state: InterpretationState
+ )(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case UBP(ub: FieldWriteAccessInformation) =>
+ accessState.fieldAccessDependee = eps.asInstanceOf[EOptionP[DeclaredField, FieldWriteAccessInformation]]
+ handleFieldAccessInformation(ub)(accessState, state)
+
+ case UBP(_: StringConstancyProperty) =>
+ accessState.updateAccessDependee(eps.asInstanceOf[EOptionP[VariableDefinition, StringConstancyProperty]])
+ tryComputeFinalResult(accessState, state)
+
+ case _ => throw new IllegalArgumentException(s"Encountered unknown eps: $eps")
+ }
+ }
+}
+
+object L3FieldReadInterpreter {
+
+ /**
+ * Checks whether the given type is supported by the field read analysis, i.e. if it may contain values desirable
+ * AND resolvable by the string analysis as a whole.
+ */
+ private def isSupportedType(fieldType: FieldType): Boolean = fieldType.isBaseType || (fieldType eq ObjectType.String)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/interpretation/L3InterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/interpretation/L3InterpretationHandler.scala
new file mode 100644
index 0000000000..c426cb393f
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l3/interpretation/L3InterpretationHandler.scala
@@ -0,0 +1,107 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package l3
+package interpretation
+
+import org.opalj.br.analyses.DeclaredFields
+import org.opalj.br.analyses.DeclaredFieldsKey
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.ContextProviderKey
+import org.opalj.br.fpcf.analyses.ContextProvider
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler
+import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState
+import org.opalj.tac.fpcf.analyses.string.l0.interpretation.BinaryExprInterpreter
+import org.opalj.tac.fpcf.analyses.string.l0.interpretation.SimpleValueConstExprInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1NonVirtualFunctionCallInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1NonVirtualMethodCallInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1StaticFunctionCallInterpreter
+import org.opalj.tac.fpcf.analyses.string.l1.interpretation.L1VirtualMethodCallInterpreter
+import org.opalj.tac.fpcf.analyses.string.l2.interpretation.L2VirtualFunctionCallInterpreter
+import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty
+
+/**
+ * @inheritdoc
+ *
+ * Interprets statements similar to [[org.opalj.tac.fpcf.analyses.string.l2.interpretation.L2InterpretationHandler]] but
+ * handles field read accesses as well.
+ *
+ * @author Maximilian Rüsch
+ */
+class L3InterpretationHandler(implicit override val project: SomeProject) extends InterpretationHandler {
+
+ implicit val declaredFields: DeclaredFields = p.get(DeclaredFieldsKey)
+ implicit val contextProvider: ContextProvider = p.get(ContextProviderKey)
+
+ override protected def processStatement(implicit
+ state: InterpretationState
+ ): Stmt[V] => ProperPropertyComputationResult = {
+ case stmt @ Assignment(_, _, expr: SimpleValueConst) =>
+ SimpleValueConstExprInterpreter.interpretExpr(stmt, expr)
+
+ // Currently unsupported
+ case Assignment(_, target, _: ArrayExpr[V]) => StringInterpreter.failure(target)
+
+ case stmt @ Assignment(_, _, expr: FieldRead[V]) =>
+ new L3FieldReadInterpreter().interpretExpr(stmt, expr)
+ // Field reads without result usage are irrelevant
+ case ExprStmt(_, _: FieldRead[V]) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+
+ case stmt: FieldWriteAccessStmt[V] =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identityForVariableAt(
+ stmt.pc,
+ stmt.value.asVar.toPersistentForm(state.tac.stmts)
+ ))
+
+ case stmt @ Assignment(_, _, expr: VirtualFunctionCall[V]) =>
+ new L2VirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+ case stmt @ ExprStmt(_, expr: VirtualFunctionCall[V]) =>
+ new L2VirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+
+ case stmt @ Assignment(_, _, expr: NonVirtualFunctionCall[V]) =>
+ L1NonVirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+ case stmt @ ExprStmt(_, expr: NonVirtualFunctionCall[V]) =>
+ L1NonVirtualFunctionCallInterpreter().interpretExpr(stmt, expr)
+
+ case stmt @ Assignment(_, _, expr: StaticFunctionCall[V]) =>
+ L1StaticFunctionCallInterpreter().interpretExpr(stmt, expr)
+ // Static function calls without return value usage are irrelevant
+ case ExprStmt(_, _: StaticFunctionCall[V]) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+
+ case stmt @ Assignment(_, _, expr: BinaryExpr[V]) => BinaryExprInterpreter().interpretExpr(stmt, expr)
+
+ case vmc: VirtualMethodCall[V] =>
+ L1VirtualMethodCallInterpreter().interpret(vmc)
+ case nvmc: NonVirtualMethodCall[V] =>
+ L1NonVirtualMethodCallInterpreter().interpret(nvmc)
+
+ case Assignment(_, _, _: New) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+
+ case Assignment(_, target, _) =>
+ StringInterpreter.failure(target)
+
+ case ReturnValue(pc, expr) =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identityForVariableAt(
+ pc,
+ expr.asVar.toPersistentForm(state.tac.stmts)
+ ))
+
+ case _ =>
+ StringInterpreter.computeFinalResult(StringFlowFunctionProperty.identity)
+ }
+}
+
+object L3InterpretationHandler {
+
+ def requiredProjectInformation: ProjectInformationKeys = Seq(DeclaredFieldsKey, ContextProviderKey)
+
+ def apply(project: SomeProject): L3InterpretationHandler = new L3InterpretationHandler()(project)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/package.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/package.scala
new file mode 100644
index 0000000000..d55bdc5ce6
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/package.scala
@@ -0,0 +1,28 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+
+import org.opalj.br.DefinedMethod
+import org.opalj.br.Method
+import org.opalj.br.fpcf.properties.Context
+
+/**
+ * @author Maximilian Rüsch
+ */
+package object string {
+
+ type TAC = TACode[TACMethodParameter, V]
+
+ private[string] case class VariableDefinition(pc: Int, pv: PV, m: Method)
+ case class VariableContext(pc: Int, pv: PV, context: Context) {
+ def m: Method = context.method.definedMethod
+ }
+
+ private[string] case class MethodParameterContext(index: Int, context: Context) {
+ def m: Method = context.method.definedMethod
+ }
+
+ case class MethodPC(pc: Int, dm: DefinedMethod)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/trivial/TrivialStringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/trivial/TrivialStringAnalysis.scala
new file mode 100644
index 0000000000..e51eced3ba
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/trivial/TrivialStringAnalysis.scala
@@ -0,0 +1,125 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package string
+package trivial
+
+import org.opalj.br.Method
+import org.opalj.br.PDVar
+import org.opalj.br.PUVar
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.BasicFPCFLazyAnalysisScheduler
+import org.opalj.br.fpcf.ContextProviderKey
+import org.opalj.br.fpcf.FPCFAnalysis
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.br.fpcf.properties.string.StringTreeConst
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.br.fpcf.properties.string.StringTreeOr
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.EPS
+import org.opalj.fpcf.InterimResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.PropertyBounds
+import org.opalj.fpcf.PropertyStore
+import org.opalj.fpcf.Result
+import org.opalj.fpcf.SomeEPS
+import org.opalj.tac.fpcf.properties.TACAI
+
+/**
+ * Provides only the most trivial information about available strings for a given variable.
+ *
+ * If the variable represents a constant string, the string value will be captured and returned in the result.
+ * If the variable represents any other value, no string value can be derived and the analysis returns either the upper
+ * or lower bound depending on the soundness mode.
+ *
+ * @see [[StringAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+class TrivialStringAnalysis(override val project: SomeProject) extends FPCFAnalysis with StringAnalysisConfig {
+
+ private case class TrivialStringAnalysisState(entity: VariableContext, var tacDependee: EOptionP[Method, TACAI])
+
+ def analyze(variableContext: VariableContext): ProperPropertyComputationResult = {
+ implicit val state: TrivialStringAnalysisState = TrivialStringAnalysisState(
+ variableContext,
+ ps(variableContext.m, TACAI.key)
+ )
+
+ if (state.tacDependee.isRefinable) {
+ InterimResult(
+ state.entity,
+ StringConstancyProperty.lb,
+ StringConstancyProperty.ub,
+ Set(state.tacDependee),
+ continuation(state)
+ )
+ } else if (state.tacDependee.ub.tac.isEmpty) {
+ // No TAC available, e.g., because the method has no body
+ Result(state.entity, StringConstancyProperty(StringInterpreter.failureTree))
+ } else {
+ determinePossibleStrings
+ }
+ }
+
+ private def continuation(state: TrivialStringAnalysisState)(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case tacaiEPS: EPS[_, _] if eps.pk == TACAI.key =>
+ state.tacDependee = tacaiEPS.asInstanceOf[EPS[Method, TACAI]]
+ determinePossibleStrings(state)
+
+ case _ =>
+ throw new IllegalArgumentException(s"Unknown EPS given in continuation: $eps")
+ }
+ }
+
+ private def determinePossibleStrings(
+ implicit state: TrivialStringAnalysisState
+ ): ProperPropertyComputationResult = {
+ val tac = state.tacDependee.ub.tac.get
+
+ def mapDefPCToStringTree(defPC: Int): StringTreeNode = {
+ if (defPC < 0) {
+ StringInterpreter.failureTree
+ } else {
+ tac.stmts(valueOriginOfPC(defPC, tac.pcToIndex).get).asAssignment.expr match {
+ case StringConst(_, v) => StringTreeConst(v)
+ case _ => StringInterpreter.failureTree
+ }
+ }
+ }
+
+ val tree = state.entity.pv match {
+ case PUVar(_, defPCs) =>
+ StringTreeOr(defPCs.map(pc => mapDefPCToStringTree(pc)))
+
+ case PDVar(_, _) =>
+ mapDefPCToStringTree(state.entity.pc)
+ }
+
+ Result(state.entity, StringConstancyProperty(tree))
+ }
+}
+
+/**
+ * @see [[TrivialStringAnalysis]]
+ *
+ * @author Maximilian Rüsch
+ */
+object LazyTrivialStringAnalysis extends BasicFPCFLazyAnalysisScheduler {
+
+ override def uses: Set[PropertyBounds] = PropertyBounds.ubs(TACAI)
+
+ override def derivesLazily: Some[PropertyBounds] = Some(PropertyBounds.lub(StringConstancyProperty))
+
+ override def requiredProjectInformation: ProjectInformationKeys = Seq(ContextProviderKey)
+
+ override def register(project: SomeProject, propertyStore: PropertyStore, i: InitializationData): FPCFAnalysis = {
+ val analysis = new TrivialStringAnalysis(project)
+ propertyStore.registerLazyPropertyComputation(StringConstancyProperty.key, analysis.analyze)
+ analysis
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/systemproperties/SystemPropertiesAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/systemproperties/SystemPropertiesAnalysis.scala
new file mode 100644
index 0000000000..f8c6eee582
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/systemproperties/SystemPropertiesAnalysis.scala
@@ -0,0 +1,162 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package systemproperties
+
+import org.opalj.br.Method
+import org.opalj.br.ObjectType
+import org.opalj.br.analyses.DeclaredMethodsKey
+import org.opalj.br.analyses.ProjectInformationKeys
+import org.opalj.br.analyses.SomeProject
+import org.opalj.br.fpcf.BasicFPCFTriggeredAnalysisScheduler
+import org.opalj.br.fpcf.properties.SystemProperties
+import org.opalj.br.fpcf.properties.cg.Callers
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.EPK
+import org.opalj.fpcf.EPS
+import org.opalj.fpcf.EUBP
+import org.opalj.fpcf.InterimEP
+import org.opalj.fpcf.InterimEUBP
+import org.opalj.fpcf.InterimPartialResult
+import org.opalj.fpcf.PartialResult
+import org.opalj.fpcf.ProperPropertyComputationResult
+import org.opalj.fpcf.PropertyBounds
+import org.opalj.fpcf.PropertyKind
+import org.opalj.fpcf.PropertyStore
+import org.opalj.fpcf.SomeEPS
+import org.opalj.fpcf.UBP
+import org.opalj.tac.cg.TypeIteratorKey
+import org.opalj.tac.fpcf.analyses.cg.ReachableMethodAnalysis
+import org.opalj.tac.fpcf.analyses.string.VariableContext
+import org.opalj.tac.fpcf.properties.TACAI
+
+/**
+ * An FPCF analysis that analyses reachable methods for calls that modify [[java.util.Properties]], including the
+ * system properties given on [[System.setProperty]].
+ *
+ * @see [[SystemProperties]]
+ *
+ * @author Maximilian Rüsch
+ */
+class SystemPropertiesAnalysis private[analyses] (
+ final val project: SomeProject
+) extends ReachableMethodAnalysis {
+
+ private type State = SystemPropertiesState[ContextType]
+
+ def processMethod(callContext: ContextType, tacaiEP: EPS[Method, TACAI]): ProperPropertyComputationResult = {
+ // IMPROVE add initialization framework similar to the EntryPointFinder framework
+ implicit val state: State = new SystemPropertiesState(callContext, tacaiEP)
+
+ var values: Set[StringTreeNode] = Set.empty
+ for (stmt <- state.tac.stmts) stmt match {
+ case VirtualFunctionCallStatement(call)
+ if (call.name == "setProperty" || call.name == "put") &&
+ classHierarchy.isSubtypeOf(call.declaringClass, ObjectType("java/util/Properties")) =>
+ values ++= getPossibleStrings(call.pc, call.params(1))
+
+ case StaticFunctionCallStatement(call)
+ if call.name == "setProperty" && call.declaringClass == ObjectType.System =>
+ values ++= getPossibleStrings(call.pc, call.params(1))
+
+ case StaticMethodCall(pc, ObjectType.System, _, "setProperty", _, params) =>
+ values ++= getPossibleStrings(pc, params(1))
+
+ case _ =>
+ }
+
+ returnResults(values)
+ }
+
+ def returnResults(values: Set[StringTreeNode])(implicit state: State): ProperPropertyComputationResult = {
+ def update(currentVal: EOptionP[SomeProject, SystemProperties]): Option[InterimEP[SomeProject, SystemProperties]] = {
+ currentVal match {
+ case UBP(ub) =>
+ val newUB = ub.mergeWith(SystemProperties(values))
+ if (newUB eq ub) None
+ else Some(InterimEUBP(project, newUB))
+
+ case _: EPK[SomeProject, SystemProperties] =>
+ Some(InterimEUBP(project, SystemProperties(values)))
+ }
+ }
+
+ if (state.hasOpenDependencies) {
+ InterimPartialResult(
+ project,
+ SystemProperties.key,
+ update,
+ state.dependees,
+ continuation(state)
+ )
+ } else {
+ PartialResult(project, SystemProperties.key, update)
+ }
+ }
+
+ def continuation(state: State)(eps: SomeEPS): ProperPropertyComputationResult = {
+ eps match {
+ case _ if eps.pk == TACAI.key =>
+ continuationForTAC(state.callContext.method)(eps)
+
+ case eps @ EUBP(_: VariableContext, ub: StringConstancyProperty) =>
+ state.updateStringDependee(eps.asInstanceOf[EPS[VariableContext, StringConstancyProperty]])
+ returnResults(Set(ub.tree))(state)
+
+ case _ =>
+ throw new IllegalArgumentException(s"unexpected eps $eps")
+ }
+ }
+
+ def getPossibleStrings(pc: Int, value: Expr[V])(implicit state: State): Set[StringTreeNode] = {
+ ps(
+ VariableContext(pc, value.asVar.toPersistentForm(state.tac.stmts), state.callContext),
+ StringConstancyProperty.key
+ ) match {
+ case eps @ UBP(ub) =>
+ state.updateStringDependee(eps)
+ Set(ub.tree)
+
+ case epk: EOptionP[VariableContext, StringConstancyProperty] =>
+ state.updateStringDependee(epk)
+ Set.empty
+ }
+ }
+}
+
+/**
+ * A scheduler for the reachable method [[SystemPropertiesAnalysis]] that is triggered on the [[Callers]] property.
+ *
+ * @author Maximilian Rüsch
+ */
+object TriggeredSystemPropertiesAnalysisScheduler extends BasicFPCFTriggeredAnalysisScheduler {
+
+ override def requiredProjectInformation: ProjectInformationKeys = Seq(DeclaredMethodsKey, TypeIteratorKey)
+
+ override def uses: Set[PropertyBounds] = PropertyBounds.ubs(
+ Callers,
+ TACAI,
+ StringConstancyProperty,
+ SystemProperties
+ )
+
+ override def triggeredBy: PropertyKind = Callers
+
+ override def register(
+ p: SomeProject,
+ ps: PropertyStore,
+ unused: Null
+ ): SystemPropertiesAnalysis = {
+ val analysis = new SystemPropertiesAnalysis(p)
+ ps.registerTriggeredComputation(Callers.key, analysis.analyze)
+ analysis
+ }
+
+ override def derivesEagerly: Set[PropertyBounds] = Set.empty
+
+ override def derivesCollaboratively: Set[PropertyBounds] = PropertyBounds.ubs(SystemProperties)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/systemproperties/SystemPropertiesState.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/systemproperties/SystemPropertiesState.scala
new file mode 100644
index 0000000000..763b77b421
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/systemproperties/SystemPropertiesState.scala
@@ -0,0 +1,40 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package analyses
+package systemproperties
+
+import org.opalj.br.Method
+import org.opalj.br.fpcf.properties.Context
+import org.opalj.br.fpcf.properties.string.StringConstancyProperty
+import org.opalj.fpcf.EOptionP
+import org.opalj.fpcf.SomeEOptionP
+import org.opalj.tac.fpcf.analyses.cg.BaseAnalysisState
+import org.opalj.tac.fpcf.analyses.string.VariableContext
+import org.opalj.tac.fpcf.properties.TACAI
+
+/**
+ * @see [[SystemPropertiesAnalysis]]
+ * @author Maximilian Rüsch
+ */
+final class SystemPropertiesState[ContextType <: Context](
+ override val callContext: ContextType,
+ override protected[this] var _tacDependee: EOptionP[Method, TACAI]
+) extends BaseAnalysisState with TACAIBasedAnalysisState[ContextType] {
+
+ private[this] var _stringConstancyDependees: Map[VariableContext, EOptionP[VariableContext, StringConstancyProperty]] =
+ Map.empty
+
+ def updateStringDependee(dependee: EOptionP[VariableContext, StringConstancyProperty]): Unit = {
+ _stringConstancyDependees = _stringConstancyDependees.updated(dependee.e, dependee)
+ }
+
+ override def hasOpenDependencies: Boolean = {
+ super.hasOpenDependencies ||
+ _stringConstancyDependees.valuesIterator.exists(_.isRefinable)
+ }
+
+ override def dependees: Set[SomeEOptionP] =
+ super.dependees ++ _stringConstancyDependees.valuesIterator.filter(_.isRefinable)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/MethodStringFlow.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/MethodStringFlow.scala
new file mode 100644
index 0000000000..6dd88960a6
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/MethodStringFlow.scala
@@ -0,0 +1,52 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package properties
+package string
+
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.fpcf.Property
+import org.opalj.fpcf.PropertyKey
+import org.opalj.fpcf.PropertyMetaInformation
+
+/**
+ * An FPCF property that captures the string flow results of an entire method based on the final [[StringTreeEnvironment]]
+ * after executing the [[org.opalj.tac.fpcf.analyses.string.flowanalysis.DataFlowAnalysis]].
+ *
+ * @see [[StringTreeEnvironment]]
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait MethodStringFlowPropertyMetaInformation extends PropertyMetaInformation {
+
+ final type Self = MethodStringFlow
+}
+
+case class MethodStringFlow(private val env: StringTreeEnvironment) extends Property
+ with MethodStringFlowPropertyMetaInformation {
+
+ final def key: PropertyKey[MethodStringFlow] = MethodStringFlow.key
+
+ def apply(pc: Int, pv: PV): StringTreeNode = env.mergeAllMatching(pc, pv)
+}
+
+object MethodStringFlow extends MethodStringFlowPropertyMetaInformation {
+
+ private final val propertyName = "opalj.MethodStringFlow"
+
+ override val key: PropertyKey[MethodStringFlow] = PropertyKey.create(propertyName)
+
+ def ub: MethodStringFlow = AllUBMethodStringFlow
+ def lb: MethodStringFlow = AllLBMethodStringFlow
+}
+
+private object AllUBMethodStringFlow extends MethodStringFlow(StringTreeEnvironment(Map.empty)) {
+
+ override def apply(pc: Int, pv: PV): StringTreeNode = StringTreeNode.ub
+}
+
+private object AllLBMethodStringFlow extends MethodStringFlow(StringTreeEnvironment(Map.empty)) {
+
+ override def apply(pc: Int, pv: PV): StringTreeNode = StringTreeNode.lb
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/StringFlowFunction.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/StringFlowFunction.scala
new file mode 100644
index 0000000000..dada85d814
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/StringFlowFunction.scala
@@ -0,0 +1,74 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package properties
+package string
+
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+import org.opalj.fpcf.Property
+import org.opalj.fpcf.PropertyKey
+import org.opalj.fpcf.PropertyMetaInformation
+
+/**
+ * An FPCF property representing a string flow function at a fixed [[org.opalj.tac.fpcf.analyses.string.MethodPC]] to be
+ * used during [[org.opalj.tac.fpcf.analyses.string.flowanalysis.DataFlowAnalysis]]. Can be produced by e.g.
+ * [[org.opalj.tac.fpcf.analyses.string.StringInterpreter]]s.
+ *
+ * @author Maximilian Rüsch
+ */
+sealed trait StringFlowFunctionPropertyMetaInformation extends PropertyMetaInformation {
+
+ final type Self = StringFlowFunctionProperty
+}
+
+case class StringFlowFunctionProperty(
+ webs: Set[PDUWeb],
+ flow: StringFlowFunction
+) extends Property
+ with StringFlowFunctionPropertyMetaInformation {
+
+ final def key: PropertyKey[StringFlowFunctionProperty] = StringFlowFunctionProperty.key
+}
+
+object StringFlowFunctionProperty extends StringFlowFunctionPropertyMetaInformation {
+
+ private final val propertyName = "opalj.StringFlowFunction"
+
+ override val key: PropertyKey[StringFlowFunctionProperty] = PropertyKey.create(propertyName)
+
+ def apply(web: PDUWeb, flow: StringFlowFunction): StringFlowFunctionProperty =
+ StringFlowFunctionProperty(Set(web), flow)
+
+ def apply(pc: Int, pv: PV, flow: StringFlowFunction): StringFlowFunctionProperty =
+ StringFlowFunctionProperty(PDUWeb(pc, pv), flow)
+
+ def lb: StringFlowFunctionProperty = constForAll(StringTreeNode.lb)
+ def ub: StringFlowFunctionProperty = constForAll(StringTreeNode.ub)
+
+ def identity: StringFlowFunctionProperty =
+ StringFlowFunctionProperty(Set.empty[PDUWeb], (env: StringTreeEnvironment) => env)
+
+ // Helps to register notable variable usage / definition which does not modify the current state
+ def identityForVariableAt(pc: Int, v: PV): StringFlowFunctionProperty =
+ StringFlowFunctionProperty(pc, v, (env: StringTreeEnvironment) => env)
+
+ def lb(pc: Int, v: PV): StringFlowFunctionProperty =
+ constForVariableAt(pc, v, StringTreeNode.lb)
+
+ def ub(pc: Int, v: PV): StringFlowFunctionProperty =
+ constForVariableAt(pc, v, StringTreeNode.ub)
+
+ def constForVariableAt(pc: Int, v: PV, result: StringTreeNode): StringFlowFunctionProperty =
+ StringFlowFunctionProperty(pc, v, (env: StringTreeEnvironment) => env.update(pc, v, result))
+
+ def constForAll(result: StringTreeNode): StringFlowFunctionProperty =
+ StringFlowFunctionProperty(Set.empty[PDUWeb], ConstForAllFlow(result))
+}
+
+trait StringFlowFunction extends (StringTreeEnvironment => StringTreeEnvironment)
+
+case class ConstForAllFlow(result: StringTreeNode) extends StringFlowFunction {
+
+ def apply(env: StringTreeEnvironment): StringTreeEnvironment = env.updateAll(result)
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/StringTreeEnvironment.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/StringTreeEnvironment.scala
new file mode 100644
index 0000000000..423e57a453
--- /dev/null
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/properties/string/StringTreeEnvironment.scala
@@ -0,0 +1,100 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package tac
+package fpcf
+package properties
+package string
+
+import org.opalj.br.PDVar
+import org.opalj.br.PUVar
+import org.opalj.br.fpcf.properties.string.SetBasedStringTreeOr
+import org.opalj.br.fpcf.properties.string.StringTreeNode
+
+/**
+ * A mapping from [[PDUWeb]] to [[StringTreeNode]], used to identify the state of string variables during a given fixed
+ * point of the [[org.opalj.tac.fpcf.analyses.string.flowanalysis.DataFlowAnalysis]].
+ *
+ * @author Maximilian Rüsch
+ */
+case class StringTreeEnvironment private (
+ private val map: Map[PDUWeb, StringTreeNode],
+ private val pcToWebs: Map[Int, Set[PDUWeb]]
+) {
+
+ private def getWebsFor(pc: Int, pv: PV): Set[PDUWeb] = {
+ pv match {
+ case _: PDVar[_] => pcToWebs(pc)
+ case _: PUVar[_] => pv.defPCs.map(pcToWebs).flatten
+ }
+ }
+
+ private def getWebsFor(web: PDUWeb): Seq[PDUWeb] = map.keys
+ .filter(_.identifiesSameVarAs(web))
+ .toSeq
+ .sortBy(_.defPCs.toList.min)
+
+ private def getWebFor(pc: Int, pv: PV): PDUWeb = {
+ val webs = getWebsFor(pc, pv)
+ webs.size match {
+ case 0 => PDUWeb(pc, pv)
+ case 1 => webs.head
+ case _ => throw new IllegalStateException("Found more than one matching web when only one should be given!")
+ }
+ }
+
+ private def getWebFor(web: PDUWeb): PDUWeb = {
+ val webs = getWebsFor(web)
+ webs.size match {
+ case 0 => web
+ case 1 => webs.head
+ case _ => throw new IllegalStateException("Found more than one matching web when only one should be given!")
+ }
+ }
+
+ def mergeAllMatching(pc: Int, pv: PV): StringTreeNode =
+ SetBasedStringTreeOr.createWithSimplify(getWebsFor(pc, pv).map(map(_)))
+
+ def apply(pc: Int, pv: PV): StringTreeNode = map(getWebFor(pc, pv))
+
+ def apply(web: PDUWeb): StringTreeNode = map(getWebFor(web))
+
+ def update(web: PDUWeb, value: StringTreeNode): StringTreeEnvironment =
+ recreate(map.updated(getWebFor(web), value))
+
+ def update(pc: Int, pv: PV, value: StringTreeNode): StringTreeEnvironment =
+ recreate(map.updated(getWebFor(pc, pv), value))
+
+ def updateAll(value: StringTreeNode): StringTreeEnvironment = recreate(map.transform((_, _) => value))
+
+ def join(other: StringTreeEnvironment): StringTreeEnvironment = {
+ recreate(map.transform { (web, tree) => SetBasedStringTreeOr.createWithSimplify(Set(tree, other.map(web))) })
+ }
+
+ def recreate(newMap: Map[PDUWeb, StringTreeNode]): StringTreeEnvironment = StringTreeEnvironment(newMap, pcToWebs)
+}
+
+object StringTreeEnvironment {
+
+ def apply(map: Map[PDUWeb, StringTreeNode]): StringTreeEnvironment = {
+ val pcToWebs =
+ map.keySet.flatMap(web => web.defPCs.map((_, web))).groupMap(_._1)(_._2)
+ .withDefaultValue(Set.empty)
+ StringTreeEnvironment(map, pcToWebs)
+ }
+
+ def joinMany(envs: Iterable[StringTreeEnvironment]): StringTreeEnvironment = {
+ // IMPROVE as environments get larger (with growing variable count in a method) joining them impacts performance
+ // especially when analysing flow through proper regions. This could be improved by detecting and joining only
+ // changes between multiple environments, e.g. with linked lists.
+ envs.size match {
+ case 0 => throw new IllegalArgumentException("Cannot join zero environments!")
+ case 1 => envs.head
+ case _ =>
+ val head = envs.head
+ // This only works as long as environment maps are not sparse
+ head.recreate(head.map.transform { (web, _) =>
+ SetBasedStringTreeOr.createWithSimplify(envs.map(_.map(web)).toSet)
+ })
+ }
+ }
+}
diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/package.scala b/OPAL/tac/src/main/scala/org/opalj/tac/package.scala
index 36eef7b3d8..3934286574 100644
--- a/OPAL/tac/src/main/scala/org/opalj/tac/package.scala
+++ b/OPAL/tac/src/main/scala/org/opalj/tac/package.scala
@@ -16,6 +16,7 @@ import org.opalj.ai.pcOfMethodExternalException
import org.opalj.br.ExceptionHandler
import org.opalj.br.ExceptionHandlers
import org.opalj.br.PCs
+import org.opalj.br.PDUVar
import org.opalj.br.PUVar
import org.opalj.br.cfg.BasicBlock
import org.opalj.br.cfg.CFG
@@ -32,6 +33,7 @@ import org.opalj.value.ValueInformation
package object tac {
type V = DUVar[ValueInformation]
+ type PV = PDUVar[ValueInformation]
final def uVarFromPersistentForm[Value <: ValueInformation](
puVar: PUVar[Value]
@@ -54,19 +56,21 @@ package object tac {
)
}
+ final def valueOriginOfPC(pc: Int, pcToIndex: Array[Int]): Option[ValueOrigin] = {
+ if (ai.underlyingPC(pc) < 0)
+ Some(pc) // parameter
+ else if (pc >= 0 && pcToIndex(pc) >= 0)
+ Some(pcToIndex(pc)) // local
+ else if (isImmediateVMException(pc) && pcToIndex(pcOfImmediateVMException(pc)) >= 0)
+ Some(ValueOriginForImmediateVMException(pcToIndex(pcOfImmediateVMException(pc))))
+ else if (isMethodExternalExceptionOrigin(pc) && pcToIndex(pcOfMethodExternalException(pc)) >= 0)
+ Some(ValueOriginForMethodExternalException(pcToIndex(pcOfMethodExternalException(pc))))
+ else
+ None
+ }
+
final def valueOriginsOfPCs(pcs: PCs, pcToIndex: Array[Int]): IntTrieSet = {
- pcs.foldLeft(EmptyIntTrieSet: IntTrieSet) { (origins, pc) =>
- if (ai.underlyingPC(pc) < 0)
- origins + pc // parameter
- else if (pc >= 0 && pcToIndex(pc) >= 0)
- origins + pcToIndex(pc) // local
- else if (isImmediateVMException(pc) && pcToIndex(pcOfImmediateVMException(pc)) >= 0)
- origins + ValueOriginForImmediateVMException(pcToIndex(pcOfImmediateVMException(pc)))
- else if (isMethodExternalExceptionOrigin(pc) && pcToIndex(pcOfMethodExternalException(pc)) >= 0)
- origins + ValueOriginForMethodExternalException(pcToIndex(pcOfMethodExternalException(pc)))
- else
- origins // as is
- }
+ pcs.foldLeft(EmptyIntTrieSet: IntTrieSet) { (origins, pc) => origins ++ valueOriginOfPC(pc, pcToIndex) }
}
/**
diff --git a/build.sbt b/build.sbt
index 4e5e85a927..032e90cd42 100644
--- a/build.sbt
+++ b/build.sbt
@@ -319,7 +319,8 @@ lazy val `ThreeAddressCode` = (project in file("OPAL/tac"))
.title("OPAL - Three Address Code") ++ Seq("-groups", "-implicits")),
assembly / assemblyJarName := "OPALTACDisassembler.jar",
assembly / mainClass := Some("org.opalj.tac.TAC"),
- run / fork := true
+ run / fork := true,
+ libraryDependencies ++= Dependencies.tac
)
.dependsOn(ai % "it->it;it->test;test->test;compile->compile")
.dependsOn(ifds % "it->it;it->test;test->test;compile->compile")
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index be2ea989e3..e76de5acba 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -25,6 +25,8 @@ object Dependencies {
val jacksonDF = "2.12.2"
val fastutil = "8.5.4"
val apkparser = "2.6.10"
+ val scalagraphcore = "2.0.1"
+ val scalagraphdot = "2.0.0"
val openjfx = "16"
@@ -56,6 +58,8 @@ object Dependencies {
val fastutil = "it.unimi.dsi" % "fastutil" % version.fastutil withSources () withJavadoc ()
val javafxBase = "org.openjfx" % "javafx-base" % version.openjfx classifier osName
val apkparser = "net.dongliu" % "apk-parser" % version.apkparser
+ val scalagraphcore = "org.scala-graph" %% "graph-core" % version.scalagraphcore
+ val scalagraphdot = "org.scala-graph" %% "graph-dot" % version.scalagraphdot
val javacpp = "org.bytedeco" % "javacpp" % version.javacpp
val javacpp_llvm = "org.bytedeco" % "llvm-platform" % (version.javacpp_llvm + "-" + version.javacpp)
@@ -76,6 +80,7 @@ object Dependencies {
val si = Seq()
val bi = Seq(commonstext)
val br = Seq(scalaparsercombinators, scalaxml)
+ val tac = Seq(scalagraphcore, scalagraphdot)
val ifds = Seq()
val tools = Seq(txtmark, jacksonDF)
val hermes = Seq(txtmark, jacksonDF, javafxBase)