diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/ClassUsageAnalysis.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/ClassUsageAnalysis.scala index 4b871d887d..805be36f21 100644 --- a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/ClassUsageAnalysis.scala +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/ClassUsageAnalysis.scala @@ -4,25 +4,22 @@ package org.opalj.support.info import scala.annotation.switch import java.net.URL -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicInteger +import scala.collection.mutable import scala.collection.mutable.ListBuffer import org.opalj.log.GlobalLogContext -import org.opalj.value.ValueInformation +import org.opalj.log.OPALLogger import org.opalj.br.analyses.BasicReport import org.opalj.br.analyses.Project import org.opalj.br.analyses.ProjectAnalysisApplication import org.opalj.br.analyses.ReportableAnalysisResult import org.opalj.tac.Assignment import org.opalj.tac.Call -import org.opalj.tac.DUVar import org.opalj.tac.ExprStmt -import org.opalj.tac.LazyDetachedTACAIKey -import org.opalj.tac.NonVirtualMethodCall -import org.opalj.tac.StaticMethodCall -import org.opalj.tac.VirtualMethodCall +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.fpcf.analyses.cg.V +import org.opalj.tac.EagerDetachedTACAIKey /** * Analyzes a project for how a particular class is used within that project. Collects information @@ -34,28 +31,41 @@ import org.opalj.tac.VirtualMethodCall * [[ClassUsageAnalysis.analysisSpecificParametersDescription]]. * * @author Patrick Mell - * @author Dominik Helm */ object ClassUsageAnalysis extends ProjectAnalysisApplication { - private type V = DUVar[ValueInformation] - implicit val logContext: GlobalLogContext.type = GlobalLogContext override def title: String = "Class Usage Analysis" override def description: String = { - "Analyzes a project for how a particular class is used within it, i.e., which methods "+ - "of instances of that class are called" + "Analysis a project for how a particular class is used, i.e., which methods are called "+ + "on it" } + /** + * The fully-qualified name of the class that is to be analyzed in a Java format, i.e., dots as + * package / class separators. + */ + private var className = "java.lang.StringBuilder" + + /** + * The analysis can run in two modes: Fine-grained or coarse-grained. Fine-grained means that + * two method are considered as the same only if their method descriptor is the same, i.e., this + * mode enables a differentiation between overloaded methods. + * The coarse-grained method, however, regards two method calls as the same if the class of the + * base object as well as the method name are equal, i.e., overloaded methods are not + * distinguished. + */ + private var isFineGrainedAnalysis = false + /** * Takes a [[Call]] and assembles the method descriptor for this call. The granularity is * determined by [[isFineGrainedAnalysis]]: For a fine-grained analysis, the returned string has * the format "[fully-qualified classname]#[method name]: [stringified method descriptor]" and * for a coarse-grained analysis: [fully-qualified classname]#[method name]. */ - private def assembleMethodDescriptor(call: Call[V], isFineGrainedAnalysis: Boolean): String = { + private def assembleMethodDescriptor(call: Call[V]): String = { val fqMethodName = s"${call.declaringClass.toJava}#${call.name}" if (isFineGrainedAnalysis) { val methodDescriptor = call.descriptor.toString @@ -66,44 +76,28 @@ object ClassUsageAnalysis extends ProjectAnalysisApplication { } /** - * Takes any [[Call]], checks whether the base object is of type [[className]] and if so, - * updates the passed map by adding the count of the corresponding method. The granularity for - * counting is determined by [[isFineGrainedAnalysis]]. + * Takes any function [[Call]], checks whether the base object is of type [[className]] and if + * so, updates the passed map by adding the count of the corresponding method. The granularity + * for counting is determined by [[isFineGrainedAnalysis]]. */ - private def processCall( - call: Call[V], - map: ConcurrentHashMap[String, AtomicInteger], - className: String, - isFineGrainedAnalysis: Boolean - ): Unit = { + private def processFunctionCall(call: Call[V], map: mutable.Map[String, Int]): Unit = { val declaringClassName = call.declaringClass.toJava if (declaringClassName == className) { - val methodDescriptor = assembleMethodDescriptor(call, isFineGrainedAnalysis) - if (map.putIfAbsent(methodDescriptor, new AtomicInteger(1)) != null) { - map.get(methodDescriptor).addAndGet(1) + val methodDescriptor = assembleMethodDescriptor(call) + if (map.contains(methodDescriptor)) { + map(methodDescriptor) += 1 + } else { + map(methodDescriptor) = 1 } } } override def analysisSpecificParametersDescription: String = { - "-class= \n"+ + "[-class= (Default: java.lang.StringBuilder)]\n"+ "[-granularity= (Default: coarse)]" } - /** - * The fully-qualified name of the class that is to be analyzed in a Java format, i.e., dots as - * package / class separators. - */ private final val parameterNameForClass = "-class=" - - /** - * The analysis can run in two modes: Fine-grained or coarse-grained. Fine-grained means that - * two methods are considered equal iff their method descriptor is the same, i.e., this mode - * enables a differentiation between overloaded methods. - * The coarse-grained method, however, regards two method calls as the same if the class of the - * base object as well as the method name are equal, i.e., overloaded methods are not - * distinguished. - */ private final val parameterNameForGranularity = "-granularity=" override def checkAnalysisSpecificParameters(parameters: Seq[String]): Traversable[String] = { @@ -118,60 +112,58 @@ object ClassUsageAnalysis extends ProjectAnalysisApplication { * Takes the parameters passed as program arguments, i.e., in the format * "-[param name]=[value]", extracts the values and sets the corresponding object variables. */ - private def getAnalysisParameters(parameters: Seq[String]): (String, Boolean) = { + private def setAnalysisParameters(parameters: Seq[String]): Unit = { val classParam = parameters.find(_.startsWith(parameterNameForClass)) - val className = if (classParam.isDefined) { - classParam.get.substring(classParam.get.indexOf("=") + 1) - } else { - throw new IllegalArgumentException("missing argument: -class") + if (classParam.isDefined) { + className = classParam.get.substring(classParam.get.indexOf("=") + 1) } val granularityParam = parameters.find(_.startsWith(parameterNameForGranularity)) - val isFineGrainedAnalysis = - if (granularityParam.isDefined) { - granularityParam.get.substring(granularityParam.get.indexOf("=") + 1) match { - case "fine" ⇒ true - case "coarse" ⇒ false - case _ ⇒ - val msg = "incorrect argument: -granularity must be one of fine|coarse" - throw new IllegalArgumentException(msg) - } + if (granularityParam.isDefined) { + val granularity = granularityParam.get.substring(granularityParam.get.indexOf("=") + 1) + if (granularity == "fine") { + isFineGrainedAnalysis = true + } else if (granularity == "coarse") { + isFineGrainedAnalysis = false } else { - false // default is coarse grained + val errMsg = s"failed parsing the granularity; it must be either 'fine' or "+ + s"'coarse' but got '$granularity'" + OPALLogger.error("fatal", errMsg) + sys.exit(2) } - - (className, isFineGrainedAnalysis) + } } override def doAnalyze( project: Project[URL], parameters: Seq[String], isInterrupted: () ⇒ Boolean ): ReportableAnalysisResult = { - val (className, isFineGrainedAnalysis) = getAnalysisParameters(parameters) - val resultMap: ConcurrentHashMap[String, AtomicInteger] = new ConcurrentHashMap() - val tacProvider = project.get(LazyDetachedTACAIKey) + setAnalysisParameters(parameters) + val resultMap = mutable.Map[String, Int]() + val tacProvider = project.get(EagerDetachedTACAIKey) - project.parForeachMethodWithBody() { methodInfo ⇒ - tacProvider(methodInfo.method).stmts.foreach { stmt ⇒ + project.allMethodsWithBody.foreach { m ⇒ + tacProvider(m).stmts.foreach { stmt ⇒ (stmt.astID: @switch) match { - case Assignment.ASTID | ExprStmt.ASTID ⇒ - stmt.asAssignmentLike.expr match { - case c: Call[V] @unchecked ⇒ - processCall(c, resultMap, className, isFineGrainedAnalysis) - case _ ⇒ - } - case NonVirtualMethodCall.ASTID | VirtualMethodCall.ASTID | - StaticMethodCall.ASTID ⇒ - processCall(stmt.asMethodCall, resultMap, className, isFineGrainedAnalysis) + case Assignment.ASTID ⇒ stmt match { + case Assignment(_, _, c: VirtualFunctionCall[V]) ⇒ + processFunctionCall(c, resultMap) + case _ ⇒ + } + case ExprStmt.ASTID ⇒ stmt match { + case ExprStmt(_, c: VirtualFunctionCall[V]) ⇒ + processFunctionCall(c, resultMap) + case _ ⇒ + } case _ ⇒ } } } - val report = ListBuffer[String]("Result:") - // Transform to a list, sort in ascending order of occurrences, and format the information - resultMap.entrySet().stream().sorted { (value1, value2) ⇒ - value1.getValue.get().compareTo(value2.getValue.get()) - }.forEach(next ⇒ report.append(s"${next.getKey}: ${next.getValue}")) + val report = ListBuffer[String]("Results") + // Transform to a list, sort in ascending order of occurrences and format the information + report.appendAll(resultMap.toList.sortWith(_._2 < _._2).map { + case (descriptor: String, count: Int) ⇒ s"$descriptor: $count" + }) BasicReport(report) } 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..4dfc5cb557 --- /dev/null +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/StringAnalysisReflectiveCalls.scala @@ -0,0 +1,311 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.support.info + +import scala.annotation.switch + +import java.net.URL + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.FinalP +import org.opalj.fpcf.InterimELUBP +import org.opalj.fpcf.InterimLUBP +import org.opalj.fpcf.InterimResult +import org.opalj.fpcf.ProperPropertyComputationResult +import org.opalj.fpcf.PropertyStore +import org.opalj.fpcf.Result +import org.opalj.fpcf.SomeEPS +import org.opalj.value.ValueInformation +import org.opalj.br.analyses.BasicReport +import org.opalj.br.analyses.Project +import org.opalj.br.analyses.ReportableAnalysisResult +import org.opalj.br.instructions.Instruction +import org.opalj.br.instructions.INVOKESTATIC +import org.opalj.br.ReferenceType +import org.opalj.br.instructions.INVOKEVIRTUAL +import org.opalj.br.Method +import org.opalj.br.analyses.ProjectAnalysisApplication +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.FPCFAnalysesManagerKey +import org.opalj.tac.Assignment +import org.opalj.tac.Call +import org.opalj.tac.ExprStmt +import org.opalj.tac.StaticFunctionCall +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.fpcf.analyses.string_analysis.P +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.DUVar +import org.opalj.tac.Stmt +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.cg.RTACallGraphKey +import org.opalj.tac.fpcf.analyses.string_analysis.LazyInterproceduralStringAnalysis + +/** + * 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. + *

+ * Currently, this runner supports / handles the following reflective calls: + *

    + *
  • `Class.forName(string)`
  • + *
  • `Class.forName(string, boolean, classLoader)`
  • + *
  • `Class.getField(string)`
  • + *
  • `Class.getDeclaredField(string)`
  • + *
  • `Class.getMethod(String, Class[])`
  • + *
  • `Class.getDeclaredMethod(String, Class[])`
  • + *
+ * + * @author Patrick Mell + */ +object StringAnalysisReflectiveCalls extends ProjectAnalysisApplication { + + private type ResultMapType = mutable.Map[String, ListBuffer[StringConstancyInformation]] + + /** + * 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 val entityContext = ListBuffer[(P, String)]() + + /** + * Stores all relevant method names of the Java Reflection API, 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]]. + */ + private val relevantMethodNames = List( + // The following is for the javax.crypto API + //"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" + // The following is for the Java Reflection API + "java.lang.Class#forName", "java.lang.ClassLoader#loadClass", + "java.lang.Class#getField", "java.lang.Class#getDeclaredField", + "java.lang.Class#getMethod", "java.lang.Class#getDeclaredMethod" + ) + + /** + * A list of fully-qualified method names that are to be skipped, e.g., because they make an + * analysis crash (e.g., com/sun/jmx/mbeanserver/MBeanInstantiator#deserialize) + */ + private val ignoreMethods = List() + + // executeFrom specifies the index / counter when to start feeding entities to the property + // store. executeTo specifies the index / counter when to stop feeding entities to the property + // store. These values are basically to help debugging. executionCounter is a helper variable + // for that purpose + private val executeFrom = 0 + private val executeTo = 10000 + private var executionCounter = 0 + + 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" + } + + /** + * 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". + */ + private 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]]. + */ + private def isRelevantCall(declaringClass: ReferenceType, methodName: String): 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]): Boolean = { + instructions.filter(_ != null).foldLeft(false) { (previous, nextInstr) ⇒ + previous || ((nextInstr.opcode: @switch) match { + case INVOKESTATIC.opcode ⇒ + val INVOKESTATIC(declClass, _, methodName, _) = nextInstr + isRelevantCall(declClass, methodName) + case INVOKEVIRTUAL.opcode ⇒ + val INVOKEVIRTUAL(declClass, methodName, _) = nextInstr + isRelevantCall(declClass, methodName) + 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( + ps: PropertyStore, method: Method, call: Call[V], resultMap: ResultMapType + ): Unit = { + if (isRelevantCall(call.declaringClass, call.name)) { + val fqnMethodName = s"${method.classFile.thisType.fqn}#${method.name}" + if (!ignoreMethods.contains(fqnMethodName)) { + if (executionCounter >= executeFrom && executionCounter <= executeTo) { + println( + s"Starting ${call.name} in ${method.classFile.thisType.fqn}#${method.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.toJava == "java.lang.String") { + val duvar = call.params(index).asVar + val e = (duvar, method) + ps.force(e, StringConstancyProperty.key) + entityContext.append( + (e, buildFQMethodName(call.declaringClass, call.name)) + ) + } + } + } + executionCounter += 1 + } + } + } + + /** + * Takes a `resultMap` and transforms the information contained in that map into a + * [[BasicReport]] which will serve as the final result of the analysis. + */ + private def resultMapToReport(resultMap: ResultMapType): BasicReport = { + val report = ListBuffer[String]("Results of the Reflection Analysis:") + for ((reflectiveCall, entries) ← resultMap) { + var constantCount, partConstantCount, dynamicCount = 0 + entries.foreach { + _.constancyLevel match { + case StringConstancyLevel.CONSTANT ⇒ constantCount += 1 + case StringConstancyLevel.PARTIALLY_CONSTANT ⇒ partConstantCount += 1 + case StringConstancyLevel.DYNAMIC ⇒ dynamicCount += 1 + } + } + + report.append(s"$reflectiveCall: ${entries.length}x") + report.append(s" -> Constant: ${constantCount}x") + report.append(s" -> Partially Constant: ${partConstantCount}x") + report.append(s" -> Dynamic: ${dynamicCount}x") + } + BasicReport(report) + } + + private def processStatements( + ps: PropertyStore, + stmts: Array[Stmt[V]], + m: Method, + resultMap: ResultMapType + ): Unit = { + stmts.foreach { stmt ⇒ + // Using the following switch speeds up the whole process + (stmt.astID: @switch) match { + case Assignment.ASTID ⇒ stmt match { + case Assignment(_, _, c: StaticFunctionCall[V]) ⇒ + processFunctionCall(ps, m, c, resultMap) + case Assignment(_, _, c: VirtualFunctionCall[V]) ⇒ + processFunctionCall(ps, m, c, resultMap) + case _ ⇒ + } + case ExprStmt.ASTID ⇒ stmt match { + case ExprStmt(_, c: StaticFunctionCall[V]) ⇒ + processFunctionCall(ps, m, c, resultMap) + case ExprStmt(_, c: VirtualFunctionCall[V]) ⇒ + processFunctionCall(ps, m, c, resultMap) + case _ ⇒ + } + case _ ⇒ + } + } + } + + private def continuation( + ps: PropertyStore, m: Method, resultMap: ResultMapType + )(eps: SomeEPS): ProperPropertyComputationResult = { + eps match { + case FinalP(tac: TACAI) ⇒ + processStatements(ps, tac.tac.get.stmts, m, resultMap) + Result(m, tac) + case InterimLUBP(lb, ub) ⇒ + InterimResult( + m, lb, ub, List(eps), continuation(ps, m, resultMap) + ) + case _ ⇒ throw new IllegalStateException("should never happen!") + } + } + + override def doAnalyze( + project: Project[URL], parameters: Seq[String], isInterrupted: () ⇒ Boolean + ): ReportableAnalysisResult = { + val manager = project.get(FPCFAnalysesManagerKey) + project.get(RTACallGraphKey) + implicit val (propertyStore, analyses) = manager.runAll( + LazyInterproceduralStringAnalysis + // LazyIntraproceduralStringAnalysis + ) + + // Stores the obtained results for each supported reflective operation + val resultMap: ResultMapType = mutable.Map[String, ListBuffer[StringConstancyInformation]]() + relevantMethodNames.foreach { resultMap(_) = ListBuffer() } + + project.allMethodsWithBody.foreach { m ⇒ + // To dramatically reduce work, quickly check if a method is relevant at all + if (instructionsContainRelevantMethod(m.body.get.instructions)) { + var tac: TACode[TACMethodParameter, DUVar[ValueInformation]] = null + val tacaiEOptP = propertyStore(m, TACAI.key) + if (tacaiEOptP.hasUBP) { + if (tacaiEOptP.ub.tac.isEmpty) { + // No TAC available, e.g., because the method has no body + println(s"No body for method: ${m.classFile.fqn}#${m.name}") + } else { + tac = tacaiEOptP.ub.tac.get + processStatements(propertyStore, tac.stmts, m, resultMap) + } + } else { + InterimResult( + m, + StringConstancyProperty.ub, + StringConstancyProperty.lb, + List(tacaiEOptP), + continuation(propertyStore, m, resultMap) + ) + } + } + } + + val t0 = System.currentTimeMillis() + propertyStore.waitOnPhaseCompletion() + entityContext.foreach { + case (e, callName) ⇒ + propertyStore.properties(e).toIndexedSeq.foreach { + case FinalP(p: StringConstancyProperty) ⇒ + resultMap(callName).append(p.stringConstancyInformation) + case InterimELUBP(_, _, ub: StringConstancyProperty) ⇒ + resultMap(callName).append(ub.stringConstancyInformation) + case _ ⇒ + println(s"Neither a final nor an interim result for $e in $callName; "+ + "this should never be the case!") + } + } + + val t1 = System.currentTimeMillis() + println(s"Elapsed Time: ${t1 - t0} ms") + resultMapToReport(resultMap) + } + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/InterproceduralTestMethods.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/InterproceduralTestMethods.java new file mode 100644 index 0000000000..47cf5c7d3f --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/InterproceduralTestMethods.java @@ -0,0 +1,698 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string_analysis; + +import org.opalj.fpcf.fixtures.string_analysis.hierarchies.GreetingService; +import org.opalj.fpcf.fixtures.string_analysis.hierarchies.HelloGreeting; +import org.opalj.fpcf.properties.string_analysis.StringDefinitions; +import org.opalj.fpcf.properties.string_analysis.StringDefinitionsCollection; + +import javax.management.remote.rmi.RMIServer; +import java.io.File; +import java.io.FileNotFoundException; +import java.lang.reflect.Method; +import java.util.Scanner; + +import static org.opalj.fpcf.properties.string_analysis.StringConstancyLevel.*; + +/** + * This file contains various tests for the InterproceduralStringAnalysis. For further information + * on what to consider, please see {@link LocalTestMethods} + * + * @author Patrick Mell + */ +public class InterproceduralTestMethods { + + public static final String JAVA_LANG = "java.lang"; + private static final String rmiServerImplStubClassName = + RMIServer.class.getName() + "Impl_Stub"; + + private String myField; + + private String noWriteField; + + private Object myObject; + + private String fieldWithInit = "init field value"; + + private String fieldWithConstructorInit; + + private float secretNumber; + + public static String someKey = "will not be revealed here"; + + private String[] monthNames = { "January", "February", "March", getApril() }; + + /** + * {@see LocalTestMethods#analyzeString} + */ + public void analyzeString(String s) { + } + + public InterproceduralTestMethods(float e) { + fieldWithConstructorInit = "initialized by constructor"; + secretNumber = e; + } + + @StringDefinitionsCollection( + value = "a case where a very simple non-virtual function call is interpreted", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(java.lang.Runtime|java.lang.StringBuilder|ERROR)" + ) + }) + public void simpleNonVirtualFunctionCallTest(int i) { + String s; + if (i == 0) { + s = getRuntimeClassName(); + } else if (i == 1) { + s = getStringBuilderClassName(); + } else { + s = "ERROR"; + } + analyzeString(s); + } + + @StringDefinitionsCollection( + value = "a case where the initialization of a StringBuilder depends on > 1 non-virtual " + + "function calls and a constant", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(java.lang.Runtime|java.lang.StringBuilder|ERROR)" + ) + }) + public void initFromNonVirtualFunctionCallTest(int i) { + String s; + if (i == 0) { + s = getRuntimeClassName(); + } else if (i == 1) { + s = getStringBuilderClassName(); + } else { + s = "ERROR"; + } + StringBuilder sb = new StringBuilder(s); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "a case where a static method with a string parameter is called", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "java.lang.(Integer|Object|Runtime)" + ) + }) + public void fromStaticMethodWithParamTest() { + analyzeString(StringProvider.getFQClassName(JAVA_LANG, "Integer")); + } + + @StringDefinitionsCollection( + value = "a case where a static method is called that returns a string but are not " + + "within this project => cannot / will not be interpret", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = ".*" + ), + + }) + public void staticMethodOutOfScopeTest() throws FileNotFoundException { + analyzeString(System.getProperty("os.version")); + } + + @StringDefinitionsCollection( + value = "a case where a (virtual) method is called that return a string but are not " + + "within this project => cannot / will not interpret", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = "(.*)*" + ) + + }) + public void methodOutOfScopeTest() 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()); + } + + @StringDefinitionsCollection( + value = "a case where an array access needs to be interpreted interprocedurally", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = "(java.lang.Object|.*|java.lang.(Integer|" + + "Object|Runtime)|.*)" + ) + + }) + public void arrayTest(int i) { + String[] classes = { + "java.lang.Object", + getRuntimeClassName(), + StringProvider.getFQClassName("java.lang", "Integer"), + System.getProperty("SomeClass") + }; + analyzeString(classes[i]); + } + + @StringDefinitionsCollection( + value = "a case that tests that the append interpretation of only intraprocedural " + + "expressions still works", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "value:(A|BC)Z" + ) + + }) + public void appendTest0(int i) { + StringBuilder sb = new StringBuilder("value:"); + if (i % 2 == 0) { + sb.append('A'); + } else { + sb.append("BC"); + } + sb.append('Z'); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "a case where function calls are involved in append operations", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "classname:StringBuilder,osname:.*" + ) + + }) + public void appendTest1() { + StringBuilder sb = new StringBuilder("classname:"); + sb.append(getSimpleStringBuilderClassName()); + sb.append(",osname:"); + sb.append(System.getProperty("os.name:")); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "a case where function calls are involved in append operations", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(java.lang.Runtime|java.lang.StringBuilder|" + + "ERROR!) - Done" + ) + + }) + public void appendTest2(int classToLoad) { + StringBuilder sb; + if (classToLoad == 0) { + sb = new StringBuilder(getRuntimeClassName()); + } else if (classToLoad == 1) { + sb = new StringBuilder(getStringBuilderClassName()); + } else { + sb = new StringBuilder("ERROR!"); + } + sb.append(" - Done"); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "a case where the concrete instance of an interface is known", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "Hello World" + ) + + }) + public void knownHierarchyInstanceTest() { + GreetingService gs = new HelloGreeting(); + analyzeString(gs.getGreeting("World")); + } + + @StringDefinitionsCollection( + value = "a case where the concrete instance of an interface is NOT known", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(Hello World|Hello)" + ) + + }) + public void unknownHierarchyInstanceTest(GreetingService greetingService) { + analyzeString(greetingService.getGreeting("World")); + } + + @StringDefinitionsCollection( + value = "a case where context-insensitivity is tested", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "java.lang.(Integer|Object|Runtime)" + ) + + }) + public void contextInsensitivityTest() { + StringBuilder sb = new StringBuilder(); + String s = StringProvider.getFQClassName("java.lang", "Object"); + sb.append(StringProvider.getFQClassName("java.lang", "Runtime")); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "a case taken from javax.management.remote.rmi.RMIConnector where a GetStatic " + + "is involved", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = ".*Impl_Stub" + ) + + }) + public void getStaticTest() { + analyzeString(rmiServerImplStubClassName); + } + + @StringDefinitionsCollection( + value = "a case where the append value has more than one def site", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "It is (great|not great)" + ) + + }) + public void appendWithTwoDefSites(int i) { + String s; + if (i > 0) { + s = "great"; + } else { + s = "not great"; + } + analyzeString(new StringBuilder("It is ").append(s).toString()); + } + + @StringDefinitionsCollection( + value = "a case where the append value has more than one def site with a function " + + "call involved", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "It is (great|Hello, World)" + ) + + }) + public void appendWithTwoDefSitesWithFuncCallTest(int i) { + String s; + if (i > 0) { + s = "great"; + } else { + s = getHelloWorld(); + } + analyzeString(new StringBuilder("It is ").append(s).toString()); + } + + @StringDefinitionsCollection( + value = "a case taken from com.sun.javafx.property.PropertyReference#reflect where " + + "a dependency within the finalize procedure is present", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "get(.*|Hello, Worldjava.lang.Runtime)" + ) + + }) + public void dependenciesWithinFinalizeTest(String s, Class clazz) { + String properName = s.length() == 1 ? s.substring(0, 1).toUpperCase() : + getHelloWorld() + getRuntimeClassName(); + String getterName = "get" + properName; + Method m; + try { + m = clazz.getMethod(getterName); + System.out.println(m); + analyzeString(getterName); + } catch (NoSuchMethodException var13) { + } + } + + @StringDefinitionsCollection( + value = "a case taken from javax.management.remote.rmi.RMIConnector where a GetStatic " + + "is involved", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "Hello, World" + ) + + }) + public String callerWithFunctionParameterTest(String s, float i) { + analyzeString(s); + return s; + } + + /** + * Necessary for the callerWithFunctionParameterTest. + */ + public void belongsToSomeTestCase() { + String s = callerWithFunctionParameterTest(belongsToTheSameTestCase(), 900); + System.out.println(s); + } + + /** + * Necessary for the callerWithFunctionParameterTest. + */ + public static String belongsToTheSameTestCase() { + return getHelloWorld(); + } + + @StringDefinitionsCollection( + value = "a case where a function takes another function as one of its parameters", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "Hello, World!" + ), + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "Hello, World?" + ) + }) + public void functionWithFunctionParameter() { + analyzeString(addExclamationMark(getHelloWorld())); + analyzeString(addQuestionMark(getHelloWorld())); + } + + @StringDefinitionsCollection( + value = "a case where no callers information need to be computed", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "java.lang.String" + ) + }) + public void noCallersInformationRequiredTest(String s) { + System.out.println(s); + analyzeString("java.lang.String"); + } + + @StringDefinitionsCollection( + value = "a case taken from com.sun.prism.impl.ps.BaseShaderContext#getPaintShader " + + "and slightly adapted", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "Hello, World_paintname((_PAD|_REFLECT|_REPEAT)?)?" + + "(_AlphaTest)?" + ) + }) + 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); + } + + /** + * Necessary for the tieName test. + */ + private static String tieNameForCompiler(String var0) { + return var0 + "_tie"; + } + + @StringDefinitionsCollection( + value = "a case where a string field is read", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(some value|another value|^null$)" + ) + }) + public void fieldReadTest() { + myField = "some value"; + analyzeString(myField); + } + + private void belongsToFieldReadTest() { + myField = "another value"; + } + + @StringDefinitionsCollection( + value = "a case where a field is read which is not written", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = "(^null$|.*)" + ) + }) + public void fieldWithNoWriteTest() { + analyzeString(noWriteField); + } + + @StringDefinitionsCollection( + value = "a case where a field is read whose type is not supported", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = ".*" + ) + }) + public void nonSupportedFieldTypeRead() { + analyzeString(myObject.toString()); + } + + @StringDefinitionsCollection( + value = "a case where a field is declared and initialized", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "init field value" + ) + }) + public void fieldWithInitTest() { + analyzeString(fieldWithInit.toString()); + } + + @StringDefinitionsCollection( + value = "a case where a field is initialized in a constructor", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "initialized by constructor" + ) + }) + public void fieldInitByConstructor() { + analyzeString(fieldWithConstructorInit.toString()); + } + + @StringDefinitionsCollection( + value = "a case where a field is initialized with a value of a constructor parameter", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = "^-?\\d*\\.{0,1}\\d+$" + ) + }) + public void fieldInitByConstructorParameter() { + analyzeString(new StringBuilder().append(secretNumber).toString()); + } + + @StringDefinitionsCollection( + value = "a case where no callers information need to be computed", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = "(.*|value)" + ) + }) + public String cyclicDependencyTest(String s) { + String value = getProperty(s); + analyzeString(value); + return value; + } + + private String getProperty(String name) { + if (name == null) { + return cyclicDependencyTest("default"); + } else { + return "value"; + } + } + + @StringDefinitionsCollection( + value = "a case where a non virtual function has multiple return values", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(One|val|java.lang.Object)" + ) + }) + public void severalReturnValuesTest1() { + analyzeString(severalReturnValuesFunction("val", 42)); + } + + /** + * Belongs to severalReturnValuesTest1. + */ + private String severalReturnValuesFunction(String s, int i) { + switch (i) { + case 0: return getObjectClassName(); + case 1: return "One"; + default: return s; + } + } + + @StringDefinitionsCollection( + value = "a case where a non static function has multiple return values", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(that's odd|my.helper.Class)" + ) + }) + public void severalReturnValuesTest2() { + analyzeString(severalReturnValuesStaticFunction(42)); + } + + /** + * Belongs to severalReturnValuesTest2. + */ + private static String severalReturnValuesStaticFunction(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 getHelperClass(); + } + } + + @StringDefinitionsCollection( + value = "a case where a non-virtual and a static function have no return values at all", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = ".*" + ), + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = ".*" + ) + }) + public void functionWithNoReturnValueTest1() { + analyzeString(noReturnFunction1()); + analyzeString(noReturnFunction2()); + } + + /** + * Belongs to functionWithNoReturnValueTest1. + */ + public String noReturnFunction1() { + throw new RuntimeException(); + } + + /** + * Belongs to functionWithNoReturnValueTest1. + */ + public static String noReturnFunction2() { + throw new RuntimeException(); + } + + @StringDefinitionsCollection( + value = "a test case which tests the interpretation of String#valueOf", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "c" + ), + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "42.3" + ), + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "java.lang.Runtime" + ) + }) + public void valueOfTest() { + analyzeString(String.valueOf('c')); + analyzeString(String.valueOf((float) 42.3)); + analyzeString(String.valueOf(getRuntimeClassName())); + } + + @StringDefinitionsCollection( + value = "a case where a static property is read", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "will not be revealed here" + ) + }) + public void getStaticFieldTest() { + analyzeString(someKey); + } + + @StringDefinitionsCollection( + value = "a case where a String array field is read", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(January|February|March|April)" + ) + }) + public void getStringArrayField(int i) { + analyzeString(monthNames[i]); + } + + private String getRuntimeClassName() { + return "java.lang.Runtime"; + } + + private String getStringBuilderClassName() { + return "java.lang.StringBuilder"; + } + + private String getSimpleStringBuilderClassName() { + return "StringBuilder"; + } + + private static String getHelloWorld() { + return "Hello, World"; + } + + private static String addExclamationMark(String s) { + return s + "!"; + } + + private String addQuestionMark(String s) { + return s + "?"; + } + + private String getObjectClassName() { + return "java.lang.Object"; + } + + private static String getHelperClass() { + return "my.helper.Class"; + } + + private String getApril() { + return "April"; + } + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/LocalTestMethods.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/LocalTestMethods.java new file mode 100644 index 0000000000..30c2c23530 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/LocalTestMethods.java @@ -0,0 +1,1015 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string_analysis; + +import org.opalj.fpcf.properties.string_analysis.StringDefinitions; +import org.opalj.fpcf.properties.string_analysis.StringDefinitionsCollection; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Random; + +import static org.opalj.fpcf.properties.string_analysis.StringConstancyLevel.*; + +/** + * This file contains various tests for the LocalStringAnalysis. The following things are to be + * considered when adding test cases: + *
    + *
  • + * The asterisk symbol (*) is used to indicate that a string (or part of it) can occur >= 0 times. + *
  • + *
  • + * Question marks (?) are used to indicate that a string (or part of it) can occur either zero + * times or once. + *
  • + *
  • + * The string "\w" is used to indicate that a string (or part of it) is unknown / arbitrary, i.e., + * it cannot be approximated. + *
  • + *
  • + * 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 https://www.freeformatter.com/java-regex-tester.html#examples 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 https://www.freeformatter.com/java-regex-tester.html#examples as + * of 2019-02-02. + *
  • + *
+ *

+ * Thus, you should avoid the following characters / strings to occur in "expectedStrings": + * {*, ?, \w, |}. In the future, "expectedStrings" might be parsed back into a StringTree. Thus, to + * be on the safe side, brackets should be avoided as well. + *

+ * On order to trigger the analysis for a particular string or String{Buffer, Builder} call the + * analyzeString method with the variable to be analyzed. It is legal to have multiple + * calls to analyzeString within the same test method. + * + * @author Patrick Mell + */ +public class LocalTestMethods { + + private String someStringField = ""; + public static final String MY_CONSTANT = "mine"; + + /** + * This method represents the test method which is serves as the trigger point for the + * {@link org.opalj.fpcf.IntraproceduralStringAnalysisTest} to know which string read operation to + * analyze. + * Note that the {@link StringDefinitions} annotation is designed in a way to be able to capture + * only one read operation. For how to get around this limitation, see the annotation. + * + * @param s Some string which is to be analyzed. + */ + public void analyzeString(String s) { + } + + @StringDefinitionsCollection( + value = "read-only string variable, trivial case", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "java.lang.String" + ), + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "java.lang.String" + ) + } + ) + public void constantStringReads() { + analyzeString("java.lang.String"); + + String className = "java.lang.String"; + analyzeString(className); + } + + @StringDefinitionsCollection( + value = "checks if a string value with append(s) is determined correctly", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "java.lang.String" + ), + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "java.lang.Object" + ) + } + ) + 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); + } + + @StringDefinitionsCollection( + value = "checks if a string value with > 2 continuous appends is determined correctly", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "java.lang.String" + ) + }) + public void directAppendConcats() { + StringBuilder sb = new StringBuilder("java"); + sb.append(".").append("lang").append(".").append("String"); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "at this point, function call cannot be handled => DYNAMIC", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, expectedStrings = ".*" + ) + }) + public void fromFunctionCall() { + String className = getStringBuilderClassName(); + analyzeString(className); + } + + @StringDefinitionsCollection( + value = "constant string + string from function call => PARTIALLY_CONSTANT", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "java.lang..*" + ) + }) + public void fromConstantAndFunctionCall() { + String className = "java.lang."; + System.out.println(className); + className += getSimpleStringBuilderClassName(); + analyzeString(className); + } + + @StringDefinitionsCollection( + value = "array access with unknown index", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "(java.lang.String|" + + "java.lang.StringBuilder|java.lang.System|java.lang.Runnable)" + ) + }) + 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]); + } + } + + @StringDefinitionsCollection( + value = "a simple case where multiple definition sites have to be considered", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(java.lang.System|java.lang.Runtime)" + ) + }) + public void multipleConstantDefSites(boolean cond) { + String s; + if (cond) { + s = "java.lang.System"; + } else { + s = "java.lang.Runtime"; + } + analyzeString(s); + } + + @StringDefinitionsCollection( + value = "a more comprehensive case where multiple definition sites have to be " + + "considered each with a different string generation mechanism", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, + expectedStrings = "((java.lang.Object|.*)|java.lang.System|" + + "java.lang..*|.*)" + ) + }) + 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); + } + + @StringDefinitionsCollection( + value = "a case where multiple optional definition sites have to be considered.", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(b|c)?" + ) + }) + public void multipleOptionalAppendSites(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()); + } + + @StringDefinitionsCollection( + value = "if-else control structure which append to a string builder with an int expr " + + "and an int", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, expectedStrings = "(x|^-?\\d+$)" + ), + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "(42-42|x)" + ) + }) + 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()); + } + + @StringDefinitionsCollection( + value = "if-else control structure which append float and double values to a string " + + "builder", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "(3.14|^-?\\d*\\.{0,1}\\d+$)2.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()); + } + + @StringDefinitionsCollection( + value = "if-else control structure which append to a string builder", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "(a|b)" + ), + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(b|c)" + ) + }) + public void ifElseWithStringBuilder1() { + 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()); + } + + @StringDefinitionsCollection( + value = "if-else control structure which append to a string builder multiple times", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(bcd|xyz)" + ) + }) + public void ifElseWithStringBuilder3() { + 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()); + } + + @StringDefinitionsCollection( + value = "simple for loops with known and unknown bounds", + stringDefinitions = { + // Currently, the analysis does not support determining loop ranges => a(b)* + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(b)*" + ), + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(b)*" + ) + }) + 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()); + } + + @StringDefinitionsCollection( + value = "if-else control structure within a for loop and with an append afterwards", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "((x|^-?\\d+$))*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()); + } + + @StringDefinitionsCollection( + value = "if control structure without an else", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(b)?" + ) + }) + public void ifWithoutElse() { + StringBuilder sb = new StringBuilder("a"); + int i = new Random().nextInt(); + if (i % 2 == 0) { + sb.append("b"); + } + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "case with a nested loop where in the outer loop a StringBuilder is created " + + "that is later read", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(b)*" + ) + }) + 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()); + } + } + + @StringDefinitionsCollection( + value = "some example that makes use of a StringBuffer instead of a StringBuilder", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "((x|^-?\\d+$))*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()); + } + + @StringDefinitionsCollection( + value = "while-true example", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(b)*" + ) + }) + public void whileWithBreak() { + StringBuilder sb = new StringBuilder("a"); + while (true) { + sb.append("b"); + if (sb.length() > 100) { + break; + } + } + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "an example with a non-while-true loop containing a break", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "a(b)*" + ) + }) + public void whileWithBreak(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()); + } + + @StringDefinitionsCollection( + value = "an extensive example with many control structures", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "(iv1|iv2): " + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "(iv1|iv2): ((great!)?)*(.*)?" + ) + }) + public void extensive(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()); + } + + @StringDefinitionsCollection( + value = "an example with a throw (and no try-catch-finally)", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "File Content:.*" + ) + }) + public void withThrow(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()); + } + + @StringDefinitionsCollection( + value = "case with a try-finally exception", + // Currently, multiple expectedLevels and expectedStrings values are necessary because + // the three-address code contains multiple calls to 'analyzeString' which are currently + // not filtered out + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "File Content:(.*)?" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "File Content:(.*)?" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "File Content:(.*)?" + ) + }) + public void withException(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()); + } + } + + @StringDefinitionsCollection( + value = "case with a try-catch-finally exception", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "=====(.*|=====)" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "=====(.*|=====)" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "=====(.*|=====)" + ) + }) + 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()); + } + } + + @StringDefinitionsCollection( + value = "case with a try-catch-finally throwable", + stringDefinitions = { + @StringDefinitions( + // Due to early stopping finding paths within DefaultPathFinder, the + // "EOS" can not be found for the first case (the difference to the case + // tryCatchFinally is that a second CatchNode is not present in the + // throwable case) + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "BOS:(.*|:EOS)" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "BOS:(.*|:EOS)" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "BOS:(.*|:EOS)" + ) + }) + 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()); + } + } + + @StringDefinitionsCollection( + value = "simple examples to clear a StringBuilder", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, expectedStrings = ".*" + ), + @StringDefinitions( + expectedLevel = DYNAMIC, expectedStrings = ".*" + ) + }) + public void simpleClearExamples() { + StringBuilder sb1 = new StringBuilder("init_value:"); + sb1.setLength(0); + sb1.append(getStringBuilderClassName()); + + StringBuilder sb2 = new StringBuilder("init_value:"); + System.out.println(sb2.toString()); + sb2 = new StringBuilder(); + sb2.append(getStringBuilderClassName()); + + analyzeString(sb1.toString()); + analyzeString(sb2.toString()); + } + + @StringDefinitionsCollection( + value = "a more advanced example with a StringBuilder#setLength to clear it", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, + expectedStrings = "(init_value:Hello, world!Goodbye|Goodbye)" + ) + }) + 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()); + } + + @StringDefinitionsCollection( + value = "a simple and a little more advanced example with a StringBuilder#replace call", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, expectedStrings = ".*" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "(init_value:Hello, world!Goodbye|.*Goodbye)" + ) + }) + public void replaceExamples(int value) { + StringBuilder sb1 = new StringBuilder("init_value"); + sb1.replace(0, 5, "replaced_value"); + 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()); + } + + @StringDefinitionsCollection( + value = "loops that use breaks and continues (or both)", + stringDefinitions = { + @StringDefinitions( + // The bytecode produces an "if" within an "if" inside the first loop, + // => two conds + expectedLevel = CONSTANT, expectedStrings = "abc(((d)?)?)*" + ), + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "" + ), + @StringDefinitions( + expectedLevel = DYNAMIC, expectedStrings = "((.*)?)*" + ) + }) + 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()); + } + + @StringDefinitionsCollection( + value = "an example where in the condition of an 'if', a string is appended to a " + + "StringBuilder", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "java.lang.Runtime" + ) + }) + 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()); + } + + @StringDefinitionsCollection( + value = "checks if a string value with > 2 continuous appends and a second " + + "StringBuilder is determined correctly", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "B." + ), + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "java.langStringB." + ) + }) + 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()); + } + + @StringDefinitionsCollection( + value = "checks if the case, where the value of a StringBuilder depends on the " + + "complex construction of a second StringBuilder is determined correctly.", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "java.lang.(Object|Runtime)" + ) + }) + public void secondStringBuilderRead(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()); + } + + @StringDefinitionsCollection( + value = "an example that uses a non final field", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "Field Value:.*" + ) + }) + public void nonFinalFieldRead() { + StringBuilder sb = new StringBuilder("Field Value:"); + System.out.println(sb); + sb.append(someStringField); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "an example that reads a public final static field; for these, the string " + + "information are available (at lease on modern compilers)", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "Field Value:mine" + ) + }) + public void finalFieldRead() { + StringBuilder sb = new StringBuilder("Field Value:"); + System.out.println(sb); + sb.append(MY_CONSTANT); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "A case with a criss-cross append on two StringBuilders", + stringDefinitions = { + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "Object(Runtime)?" + ), + @StringDefinitions( + expectedLevel = CONSTANT, expectedStrings = "Runtime(Object)?" + ) + }) + 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()); + } + + @StringDefinitionsCollection( + value = "examples that use a passed parameter to define strings that are analyzed", + stringDefinitions = { + @StringDefinitions( + expectedLevel = DYNAMIC, expectedStrings = ".*" + ), + @StringDefinitions( + expectedLevel = DYNAMIC, expectedStrings = ".*" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "value=.*" + ), + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, expectedStrings = "value=.*.*" + ) + }) + 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(sbValue.toString()); + analyzeString(sb.toString()); + } + + @StringDefinitionsCollection( + value = "an example extracted from " + + "com.oracle.webservices.internal.api.message.BasePropertySet with two " + + "definition sites and one usage site", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "(set.*|s.*)" + ), + }) + public void twoDefinitionsOneUsage(String getName) throws ClassNotFoundException { + String name = getName; + String setName = name.startsWith("is") ? + "set" + name.substring(2) : + 's' + name.substring(1); + + Class clazz = Class.forName("java.lang.MyClass"); + Method setter; + try { + setter = clazz.getMethod(setName); + analyzeString(setName); + } catch (NoSuchMethodException var15) { + setter = null; + System.out.println("Error occurred"); + } + } + + @StringDefinitionsCollection( + value = "Some comprehensive example for experimental purposes taken from the JDK and " + + "slightly modified", + stringDefinitions = { + @StringDefinitions( + expectedLevel = PARTIALLY_CONSTANT, + expectedStrings = "Hello: (.*|.*|.*)?" + ), + }) + 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(); + // TODO: Control structures in finally blocks are not handles correctly + // if (i % 2 == 0) { + // System.out.println("Ready to analyze now in any case!" + i); + // } + } + + analyzeString(sb.toString()); + } + } + + @StringDefinitionsCollection( + value = "an example with an unknown character read", + stringDefinitions = { + @StringDefinitions(expectedLevel = DYNAMIC, expectedStrings = ".*"), + @StringDefinitions(expectedLevel = DYNAMIC, expectedStrings = ".*"), + }) + 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()); + } + + // @StringDefinitions( + // value = "a case with a switch with missing breaks", + // expectedLevel = StringConstancyLevel.CONSTANT}, + // expectedStrings ={ "a(bc|c)?" } + // ) + // public void switchWithMissingBreak(int value) { + // StringBuilder sb = new StringBuilder("a"); + // switch (value) { + // case 0: + // sb.append("b"); + // case 1: + // sb.append("c"); + // break; + // case 2: + // break; + // } + // 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_analysis/StringProvider.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/StringProvider.java new file mode 100644 index 0000000000..59ba4b4573 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/StringProvider.java @@ -0,0 +1,13 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string_analysis; + +public class StringProvider { + + /** + * Returns "[packageName].[className]". + */ + public static String getFQClassName(String packageName, String className) { + return packageName + "." + className; + } + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/GreetingService.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/GreetingService.java new file mode 100644 index 0000000000..65ecf9b691 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/GreetingService.java @@ -0,0 +1,9 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string_analysis.hierarchies; + +public interface GreetingService { + + String getGreeting(String name); + +} + diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/HelloGreeting.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/HelloGreeting.java new file mode 100644 index 0000000000..ca9cb0c921 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/HelloGreeting.java @@ -0,0 +1,11 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string_analysis.hierarchies; + +public class HelloGreeting implements GreetingService { + + @Override + public String getGreeting(String name) { + return "Hello " + name; + } + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/SimpleHelloGreeting.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/SimpleHelloGreeting.java new file mode 100644 index 0000000000..af86ec1dab --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string_analysis/hierarchies/SimpleHelloGreeting.java @@ -0,0 +1,11 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string_analysis.hierarchies; + +public class SimpleHelloGreeting implements GreetingService { + + @Override + public String getGreeting(String name) { + return "Hello"; + } + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringConstancyLevel.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringConstancyLevel.java new file mode 100644 index 0000000000..8b3ac41cce --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringConstancyLevel.java @@ -0,0 +1,27 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string_analysis; + +/** + * Java annotations do not work with Scala enums, such as + * {@link org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel}. Thus, this enum. + * + * @author Patrick Mell + */ +public enum StringConstancyLevel { + + // For details, see {@link org.opalj.fpcf.properties.StringConstancyLevel}. + CONSTANT("constant"), + PARTIALLY_CONSTANT("partially_constant"), + DYNAMIC("dynamic"); + + private final String value; + + StringConstancyLevel(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringDefinitions.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringDefinitions.java new file mode 100644 index 0000000000..d8512e51ce --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringDefinitions.java @@ -0,0 +1,41 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string_analysis; + +import org.opalj.fpcf.properties.PropertyValidator; + +import java.lang.annotation.*; + +/** + * The StringDefinitions annotation states how a string field or local variable looks like with + * respect to the possible string values that can be read as well as the constancy level, i.e., + * whether the string contains only constan or only dynamic parts or a mixture. + *

+ * Note that the {@link StringDefinitions} annotation is designed in a way to be able to capture + * only one read operation per test method. If this is a limitation, either (1) duplicate the + * corresponding test method and remove the first calls which trigger the analysis or (2) put the + * relevant code of the test function into a dedicated function and then call it from different + * test methods (to avoid copy&paste). + * + * @author Patrick Mell + */ +@PropertyValidator(key = "StringConstancy", validator = StringAnalysisMatcher.class) +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.ANNOTATION_TYPE }) +public @interface StringDefinitions { + + /** + * This value determines the expected level of freedom for a local variable to + * be changed. + */ + StringConstancyLevel expectedLevel(); + + /** + * A regexp like string that describes the element(s) that are expected. For the rules, refer to + * {@link org.opalj.fpcf.fixtures.string_analysis.LocalTestMethods}. + * For example, "(* | (hello | world)^5)" describes a string which can 1) either be any string + * or 2) a five time concatenation of "hello" and/or "world". + */ + String expectedStrings(); + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringDefinitionsCollection.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringDefinitionsCollection.java new file mode 100644 index 0000000000..deadbc9349 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string_analysis/StringDefinitionsCollection.java @@ -0,0 +1,28 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string_analysis; + +import java.lang.annotation.*; + +/** + * A test method can contain > 1 triggers for analyzing a variable. Thus, multiple results are + * expected. This annotation is a wrapper for these expected results. For further information see + * {@link StringDefinitions}. + * + * @author Patrick Mell + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface StringDefinitionsCollection { + + /** + * A short reasoning of this property. + */ + String value() default "N/A"; + + /** + * The expected results in the correct order. + */ + StringDefinitions[] stringDefinitions(); + +} 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..b359494a68 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/StringAnalysisTest.scala @@ -0,0 +1,252 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package fpcf + +import java.io.File +import java.net.URL + +import scala.collection.mutable.ListBuffer + +import org.opalj.collection.immutable.Chain +import org.opalj.br.analyses.Project +import org.opalj.br.Annotation +import org.opalj.br.Method +import org.opalj.br.cfg.CFG +import org.opalj.br.Annotations +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.FPCFAnalysesManagerKey +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.PropertyStoreKey +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.VirtualMethodCall +import org.opalj.tac.fpcf.analyses.string_analysis.LazyInterproceduralStringAnalysis +import org.opalj.tac.fpcf.analyses.string_analysis.LazyIntraproceduralStringAnalysis +import org.opalj.tac.EagerDetachedTACAIKey +import org.opalj.tac.cg.RTACallGraphKey +import org.opalj.tac.fpcf.analyses.string_analysis.V + +/** + * @param fqTestMethodsClass The fully-qualified name of the class that contains the test methods. + * @param nameTestMethod The name of the method from which to extract DUVars to analyze. + * @param filesToLoad Necessary (test) files / classes to load. Note that this list should not + * include "StringDefinitions.class" as this class is loaded by default. + */ +sealed class StringAnalysisTestRunner( + val fqTestMethodsClass: String, + val nameTestMethod: String, + val filesToLoad: List[String] +) extends PropertiesTest { + + /** + * @return Returns all relevant project files (NOT including library files) to run the tests. + */ + def getRelevantProjectFiles: Array[File] = { + val necessaryFiles = Array( + "properties/string_analysis/StringDefinitions.class" + ) ++ filesToLoad + val basePath = System.getProperty("user.dir")+ + "/DEVELOPING_OPAL/validate/target/scala-2.12/test-classes/org/opalj/fpcf/" + + necessaryFiles.map { filePath ⇒ new File(basePath + filePath) } + } + + /** + * Extracts a `StringDefinitions` annotation from a `StringDefinitionsCollection` annotation. + * Make sure that you pass an instance of `StringDefinitionsCollection` and that the element at + * the given index really exists. Otherwise an exception will be thrown. + * + * @param a The `StringDefinitionsCollection` to extract a `StringDefinitions` from. + * @param index The index of the element from the `StringDefinitionsCollection` annotation to + * get. + * @return Returns the desired `StringDefinitions` annotation. + */ + def getStringDefinitionsFromCollection(a: Annotations, index: Int): Annotation = + a.head.elementValuePairs(1).value.asArrayValue.values(index).asAnnotationValue.annotation + + def determineEntitiesToAnalyze( + project: Project[URL] + ): Iterable[(V, Method)] = { + val entitiesToAnalyze = ListBuffer[(V, Method)]() + val tacProvider = project.get(EagerDetachedTACAIKey) + project.allMethodsWithBody.filter { + _.runtimeInvisibleAnnotations.foldLeft(false)( + (exists, a) ⇒ exists || StringAnalysisTestRunner.isStringUsageAnnotation(a) + ) + } foreach { m ⇒ + StringAnalysisTestRunner.extractUVars( + tacProvider(m).cfg, fqTestMethodsClass, nameTestMethod + ).foreach { uvar ⇒ + entitiesToAnalyze.append((uvar, m)) + } + } + entitiesToAnalyze + } + + def determineEAS( + entities: Iterable[(V, Method)], + project: Project[URL] + ): Traversable[((V, Method), String ⇒ String, List[Annotation])] = { + val m2e = entities.groupBy(_._2).iterator.map(e ⇒ e._1 → e._2.map(k ⇒ k._1)).toMap + // As entity, we need not the method but a tuple (DUVar, Method), thus this transformation + + val eas = methodsWithAnnotations(project).filter(am ⇒ m2e.contains(am._1)).flatMap { am ⇒ + m2e(am._1).zipWithIndex.map { + case (duvar, index) ⇒ + Tuple3( + (duvar, am._1), + { s: String ⇒ s"${am._2(s)} (#$index)" }, + List(getStringDefinitionsFromCollection(am._3, index)) + ) + } + } + + eas + } +} + +object StringAnalysisTestRunner { + + private val fqStringDefAnnotation = + "org.opalj.fpcf.properties.string_analysis.StringDefinitionsCollection" + + /** + * Takes an annotation and checks if it is a + * [[org.opalj.fpcf.properties.string_analysis.StringDefinitions]] annotation. + * + * @param a The annotation to check. + * @return True if the `a` is of type StringDefinitions and false otherwise. + */ + def isStringUsageAnnotation(a: Annotation): Boolean = + a.annotationType.toJavaClass.getName == fqStringDefAnnotation + + /** + * Extracts [[org.opalj.tac.UVar]]s from a set of statements. The locations of the UVars are + * identified by the argument to the very first call to LocalTestMethods#analyzeString. + * + * @param cfg The control flow graph from which to extract the UVar, usually derived from the + * method that contains the call(s) to LocalTestMethods#analyzeString. + * @return Returns the arguments of the LocalTestMethods#analyzeString as a DUVars list in the + * order in which they occurred in the given statements. + */ + def extractUVars( + cfg: CFG[Stmt[V], TACStmts[V]], + fqTestMethodsClass: String, + nameTestMethod: String + ): List[V] = { + cfg.code.instructions.filter { + case VirtualMethodCall(_, declClass, _, name, _, _, _) ⇒ + declClass.toJavaClass.getName == fqTestMethodsClass && name == nameTestMethod + case _ ⇒ false + }.map(_.asVirtualMethodCall.params.head.asVar).toList + } + +} + +/** + * Tests whether the [[IntraproceduralStringAnalysis]] works correctly with respect to some + * well-defined tests. + * + * @author Patrick Mell + */ +class IntraproceduralStringAnalysisTest extends PropertiesTest { + + describe("the org.opalj.fpcf.LocalStringAnalysis is started") { + val runner = new StringAnalysisTestRunner( + IntraproceduralStringAnalysisTest.fqTestMethodsClass, + IntraproceduralStringAnalysisTest.nameTestMethod, + IntraproceduralStringAnalysisTest.filesToLoad + ) + val p = Project(runner.getRelevantProjectFiles, Array[File]()) + + val manager = p.get(FPCFAnalysesManagerKey) + val ps = p.get(PropertyStoreKey) + val entities = runner.determineEntitiesToAnalyze(p) + val (_, analyses) = manager.runAll( + List(LazyIntraproceduralStringAnalysis), + { _: Chain[ComputationSpecification[FPCFAnalysis]] ⇒ + entities.foreach(ps.force(_, StringConstancyProperty.key)) + } + ) + + val testContext = TestContext(p, ps, analyses.map(_._2)) + + val eas = runner.determineEAS(entities, p) + + ps.shutdown() + validateProperties(testContext, eas, Set("StringConstancy")) + ps.waitOnPhaseCompletion() + } + +} + +object IntraproceduralStringAnalysisTest { + + val fqTestMethodsClass = "org.opalj.fpcf.fixtures.string_analysis.LocalTestMethods" + // The name of the method from which to extract DUVars to analyze + val nameTestMethod = "analyzeString" + // Files to load for the runner + val filesToLoad = List( + "fixtures/string_analysis/LocalTestMethods.class" + ) + +} + +/** + * Tests whether the InterproceduralStringAnalysis works correctly with respect to some + * well-defined tests. + * + * @author Patrick Mell + */ +class InterproceduralStringAnalysisTest extends PropertiesTest { + + describe("the org.opalj.fpcf.InterproceduralStringAnalysis is started") { + val runner = new StringAnalysisTestRunner( + InterproceduralStringAnalysisTest.fqTestMethodsClass, + InterproceduralStringAnalysisTest.nameTestMethod, + InterproceduralStringAnalysisTest.filesToLoad + ) + val p = Project(runner.getRelevantProjectFiles, Array[File]()) + + val entities = runner.determineEntitiesToAnalyze(p) + + p.get(RTACallGraphKey) + val ps = p.get(PropertyStoreKey) + val manager = p.get(FPCFAnalysesManagerKey) + val analysesToRun = Set( + LazyInterproceduralStringAnalysis + ) + + val (_, analyses) = manager.runAll( + analysesToRun, + { _: Chain[ComputationSpecification[FPCFAnalysis]] ⇒ + entities.foreach(ps.force(_, StringConstancyProperty.key)) + } + ) + + val testContext = TestContext(p, ps, analyses.map(_._2)) + val eas = runner.determineEAS(entities, p) + + ps.waitOnPhaseCompletion() + ps.shutdown() + + validateProperties(testContext, eas, Set("StringConstancy")) + } + +} + +object InterproceduralStringAnalysisTest { + + val fqTestMethodsClass = "org.opalj.fpcf.fixtures.string_analysis.InterproceduralTestMethods" + // The name of the method from which to extract DUVars to analyze + val nameTestMethod = "analyzeString" + // Files to load for the runner + val filesToLoad = List( + "fixtures/string_analysis/InterproceduralTestMethods.class", + "fixtures/string_analysis/StringProvider.class", + "fixtures/string_analysis/hierarchies/GreetingService.class", + "fixtures/string_analysis/hierarchies/HelloGreeting.class", + "fixtures/string_analysis/hierarchies/SimpleHelloGreeting.class" + ) + +} diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string_analysis/StringAnalysisMatcher.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string_analysis/StringAnalysisMatcher.scala new file mode 100644 index 0000000000..aa5515d54f --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string_analysis/StringAnalysisMatcher.scala @@ -0,0 +1,84 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string_analysis + +import org.opalj.br.analyses.Project +import org.opalj.br.AnnotationLike +import org.opalj.br.ObjectType +import org.opalj.fpcf.properties.AbstractPropertyMatcher +import org.opalj.fpcf.Property +import org.opalj.br.fpcf.properties.StringConstancyProperty + +/** + * Matches local variable's `StringConstancy` property. The match is successful if the + * variable has a constancy level that matches its actual usage and the expected values are present. + * + * @author Patrick Mell + */ +class StringAnalysisMatcher extends AbstractPropertyMatcher { + + /** + * @param a An annotation like of type + * [[org.opalj.fpcf.properties.string_analysis.StringDefinitions]]. + * + * @return Returns the constancy level specified in the annotation as a string. In case an + * annotation other than StringDefinitions is passed, an [[IllegalArgumentException]] + * will be thrown (since it cannot be processed). + */ + private def getConstancyLevel(a: AnnotationLike): String = { + a.elementValuePairs.find(_.name == "expectedLevel") match { + case Some(el) ⇒ el.value.asEnumValue.constName + case None ⇒ throw new IllegalArgumentException( + "Can only extract the constancy level from a StringDefinitions annotation" + ) + } + } + + /** + * @param a An annotation like of type + * [[org.opalj.fpcf.properties.string_analysis.StringDefinitions]]. + * + * @return Returns the ''expectedStrings'' value from the annotation. In case an annotation + * other than StringDefinitions is passed, an [[IllegalArgumentException]] will be + * thrown (since it cannot be processed). + */ + private def getExpectedStrings(a: AnnotationLike): String = { + a.elementValuePairs.find(_.name == "expectedStrings") match { + case Some(el) ⇒ el.value.asStringValue.value + case None ⇒ throw new IllegalArgumentException( + "Can only extract the possible strings from a StringDefinitions annotation" + ) + } + } + + /** + * @inheritdoc + */ + override def validateProperty( + p: Project[_], + as: Set[ObjectType], + entity: Any, + a: AnnotationLike, + properties: Traversable[Property] + ): Option[String] = { + var actLevel = "" + var actString = "" + properties.head match { + case prop: StringConstancyProperty ⇒ + val sci = prop.stringConstancyInformation + actLevel = sci.constancyLevel.toString.toLowerCase + actString = sci.possibleStrings + case _ ⇒ + } + + val expLevel = getConstancyLevel(a).toLowerCase + val expStrings = getExpectedStrings(a) + val errorMsg = s"Level: $expLevel, Strings: $expStrings" + + if (expLevel != actLevel || expStrings != actString) { + return Some(errorMsg) + } + + None + } + +} diff --git a/OPAL/br/src/main/scala/org/opalj/br/cfg/CFG.scala b/OPAL/br/src/main/scala/org/opalj/br/cfg/CFG.scala index cfb0b0d915..8b592e6ff5 100644 --- a/OPAL/br/src/main/scala/org/opalj/br/cfg/CFG.scala +++ b/OPAL/br/src/main/scala/org/opalj/br/cfg/CFG.scala @@ -7,6 +7,8 @@ import scala.reflect.ClassTag import java.util.Arrays +import org.opalj.collection.immutable.EmptyIntTrieSet + import scala.collection.{Set ⇒ SomeSet} import scala.collection.AbstractIterator @@ -20,6 +22,10 @@ import org.opalj.collection.mutable.IntArrayStack import org.opalj.graphs.DefaultMutableNode import org.opalj.graphs.DominatorTree import org.opalj.graphs.Node +import org.opalj.graphs.PostDominatorTree + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer /** * Represents the control flow graph of a method. @@ -722,6 +728,94 @@ case class CFG[I <: AnyRef, C <: CodeSequence[I]]( ) } + // We use this variable for caching, as the loop information of a CFG are permanent and do not + // need to be recomputed (see findNaturalLoops for usage) + private var naturalLoops: Option[List[List[Int]]] = None + + /** + * ''findNaturalLoops'' finds all natural loops in this dominator tree and returns them as a + * list of lists where each inner list represents one loop and the Int values correspond to the + * indices of the nodes. + * + * @return Returns all found loops. The structure of the inner lists is as follows: The first + * element of each inner list, i.e., each loop, is the loop header and the very last + * element is the one that has a back-edge to the loop header. In between, elements are + * ordered according to their occurrences, i.e., if ''n1'' is executed before ''n2'', + * the index of ''n1'' is less than the index of ''n2''. + * @note This function only focuses on natural loops, i.e., it may / will produce incorrect + * results on irreducible loops. For further information, see + * [[http://www.cs.princeton.edu/courses/archive/spring03/cs320/notes/loops.pdf]]. + */ + def findNaturalLoops(): List[List[Int]] = { + // Find loops only if that has not been done before + if (naturalLoops.isEmpty) { + val domTree = dominatorTree + // Execute a depth-first-search to find all back-edges + val start = startBlock.startPC + val seenNodes = ListBuffer[Int](start) + val toVisitStack = mutable.Stack[Int](successors(start).toArray: _*) + // backedges stores all back-edges in the form (from, to) (where to dominates from) + val backedges = ListBuffer[(Int, Int)]() + while (toVisitStack.nonEmpty) { + val from = toVisitStack.pop() + val to = successors(from).toArray + // Check for back-edges (exclude catch nodes here as this would detect loops where + // no actually are + to.filter { next ⇒ + val index = seenNodes.indexOf(next) + val isCatchNode = catchNodes.exists(_.handlerPC == next) + index > -1 && !isCatchNode && domTree.doesDominate(seenNodes(index), from) + }.foreach { destIndex ⇒ + // There are loops that have more than one edge leaving the loop; let x denote + // the loop header and y1, y2 two edges that leave the loop with y1 happens + // before y2; this method only saves one loop per loop header, thus y1 is + // removed as it is still implicitly contained in the loop denoted by x to y2 + // (note that this does not apply for nested loops, they are kept) + val hasDest = backedges.exists(_._2 == destIndex) + var removedBackedge = false + backedges.filter { + case (oldTo: Int, oldFrom: Int) ⇒ oldFrom == destIndex && oldTo < from + }.foreach { toRemove ⇒ removedBackedge = true; backedges -= toRemove } + if (!hasDest || removedBackedge) { + backedges.append((from, destIndex)) + } + } + + seenNodes.append(from) + toVisitStack.pushAll(to.filter(!seenNodes.contains(_))) + } + + // Finally, assemble the lists of loop elements + naturalLoops = Some(backedges.map { case (dest, root) ⇒ root.to(dest).toList }.toList) + } + + naturalLoops.get + } + + /** + * @return Returns the post dominator tree of this CFG. + * + * @see [[PostDominatorTree.apply]] + */ + def postDominatorTree: PostDominatorTree = { + val exitNodes = basicBlocks.zipWithIndex.filter { next ⇒ + next._1.successors.size == 1 && next._1.successors.head.isInstanceOf[ExitNode] + }.map(_._2) + PostDominatorTree( + if (exitNodes.length == 1) Some(exitNodes.head) else None, + i ⇒ exitNodes.contains(i), + // TODO: Pass an IntTrieSet if exitNodes contains more than one element + EmptyIntTrieSet, + // TODO: Correct function (just copied it from one of the tests)? + (f: Int ⇒ Unit) ⇒ exitNodes.foreach(e ⇒ f(e)), + foreachSuccessor, + foreachPredecessor, + basicBlocks.foldLeft(0) { (prevMaxNode: Int, next: BasicBlock) ⇒ + math.max(prevMaxNode, next.endPC) + } + ) + } + // --------------------------------------------------------------------------------------------- // // Visualization & Debugging diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/EscapeProperty.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/EscapeProperty.scala index 09eb301f2b..231bca6819 100644 --- a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/EscapeProperty.scala +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/EscapeProperty.scala @@ -187,8 +187,10 @@ object EscapeProperty extends EscapePropertyMetaInformation { final val Name = "opalj.EscapeProperty" - final lazy val key: PropertyKey[EscapeProperty] = PropertyKey.create(Name, AtMost(NoEscape)) - + final lazy val key: PropertyKey[EscapeProperty] = PropertyKey.create( + Name, + AtMost(NoEscape) + ) } /** diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/ReturnValueFreshness.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/ReturnValueFreshness.scala index 39b06bdda4..dbe7f255a8 100644 --- a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/ReturnValueFreshness.scala +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/ReturnValueFreshness.scala @@ -45,7 +45,6 @@ object ReturnValueFreshness extends ReturnValueFreshnessPropertyMetaInformation // fallback value NoFreshReturnValue ) - } /** diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/StringConstancyProperty.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/StringConstancyProperty.scala new file mode 100644 index 0000000000..983a5c5adb --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/StringConstancyProperty.scala @@ -0,0 +1,85 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.br.fpcf.properties + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.FallbackReason +import org.opalj.fpcf.Property +import org.opalj.fpcf.PropertyKey +import org.opalj.fpcf.PropertyMetaInformation +import org.opalj.fpcf.PropertyStore +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType + +sealed trait StringConstancyPropertyMetaInformation extends PropertyMetaInformation { + final type Self = StringConstancyProperty +} + +class StringConstancyProperty( + val stringConstancyInformation: StringConstancyInformation +) extends Property with StringConstancyPropertyMetaInformation { + + final def key: PropertyKey[StringConstancyProperty] = StringConstancyProperty.key + + override def toString: String = { + val level = stringConstancyInformation.constancyLevel.toString.toLowerCase + s"Level: $level, Possible Strings: ${stringConstancyInformation.possibleStrings}" + } + + /** + * @return Returns `true` if the [[stringConstancyInformation]] contained in this instance is + * the neutral element (see [[StringConstancyInformation.isTheNeutralElement]]). + */ + def isTheNeutralElement: Boolean = { + stringConstancyInformation.isTheNeutralElement + } + + override def hashCode(): Int = stringConstancyInformation.hashCode() + + override def equals(o: Any): Boolean = o match { + case scp: StringConstancyProperty ⇒ + stringConstancyInformation.equals(scp.stringConstancyInformation) + case _ ⇒ false + } +} + +object StringConstancyProperty extends Property with StringConstancyPropertyMetaInformation { + + final val PropertyKeyName = "StringConstancy" + + final val key: PropertyKey[StringConstancyProperty] = { + PropertyKey.create( + PropertyKeyName, + (_: PropertyStore, _: FallbackReason, _: Entity) ⇒ { + // TODO: Using simple heuristics, return a better value for some easy cases + lb + } + ) + } + + def apply( + stringConstancyInformation: StringConstancyInformation + ): StringConstancyProperty = new StringConstancyProperty(stringConstancyInformation) + + /** + * @return Returns the / a neutral [[StringConstancyProperty]] element, i.e., an element for + * which [[StringConstancyProperty.isTheNeutralElement]] is `true`. + */ + def getNeutralElement: StringConstancyProperty = + StringConstancyProperty(StringConstancyInformation.getNeutralElement) + + /** + * @return Returns the upper bound from a lattice-point of view. + */ + def ub: StringConstancyProperty = + StringConstancyProperty(StringConstancyInformation( + StringConstancyLevel.CONSTANT, StringConstancyType.APPEND + )) + + /** + * @return Returns the lower bound from a lattice-point of view. + */ + def lb: StringConstancyProperty = + StringConstancyProperty(StringConstancyInformation.lb) + +} diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyInformation.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyInformation.scala new file mode 100644 index 0000000000..d481043094 --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyInformation.scala @@ -0,0 +1,119 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.br.fpcf.properties.string_definition + +/** + * @param possibleStrings Only relevant for some [[StringConstancyType]]s, i.e., sometimes this + * parameter can be omitted. + * + * @author Patrick Mell + */ +case class StringConstancyInformation( + constancyLevel: StringConstancyLevel.Value = StringConstancyLevel.DYNAMIC, + constancyType: StringConstancyType.Value = StringConstancyType.APPEND, + possibleStrings: String = "" +) { + + /** + * Checks whether the instance is the neutral element. + * + * @return Returns `true` iff [[constancyLevel]] equals [[StringConstancyLevel.CONSTANT]], + * [[constancyType]] equals [[StringConstancyType.APPEND]], and + * [[possibleStrings]] equals the empty string. + */ + def isTheNeutralElement: Boolean = + constancyLevel == StringConstancyLevel.CONSTANT && + constancyType == StringConstancyType.APPEND && + possibleStrings == "" + +} + +/** + * Provides a collection of instance-independent but string-constancy related values. + */ +object StringConstancyInformation { + + /** + * This string stores the value that is to be used when a string is dynamic, i.e., can have + * arbitrary values. + */ + val UnknownWordSymbol: String = ".*" + + /** + * The stringified version of a (dynamic) integer value. + */ + val IntValue: String = "^-?\\d+$" + + /** + * The stringified version of a (dynamic) float value. + */ + val FloatValue: String = "^-?\\d*\\.{0,1}\\d+$" + + /** + * A value to be used when the number of an element, that is repeated, is unknown. + */ + val InfiniteRepetitionSymbol: String = "*" + + /** + * A value to be used to indicate that a string expression might be null. + */ + val NullStringValue: String = "^null$" + + /** + * Takes a list of [[StringConstancyInformation]] and reduces them to a single one by or-ing + * them together (the level is determined by finding the most general level; the type is set to + * [[StringConstancyType.APPEND]] and the possible strings are concatenated using a pipe and + * then enclosed by brackets. + * + * @param scis The information to reduce. If a list with one element is passed, this element is + * returned (without being modified in any way); a list with > 1 element is reduced + * as described above; the empty list will throw an error! + * @return Returns the reduced information in the fashion described above. + */ + def reduceMultiple(scis: Iterable[StringConstancyInformation]): StringConstancyInformation = { + val relScis = scis.filter(!_.isTheNeutralElement) + relScis.size match { + // The list may be empty, e.g., if the UVar passed to the analysis, refers to a + // VirtualFunctionCall (they are not interpreted => an empty list is returned) => return + // the neutral element + case 0 ⇒ StringConstancyInformation.getNeutralElement + case 1 ⇒ relScis.head + case _ ⇒ // Reduce + val reduced = relScis.reduceLeft((o, n) ⇒ + StringConstancyInformation( + StringConstancyLevel.determineMoreGeneral( + o.constancyLevel, n.constancyLevel + ), + StringConstancyType.APPEND, + s"${o.possibleStrings}|${n.possibleStrings}" + )) + // Add parentheses to possibleStrings value (to indicate a choice) + StringConstancyInformation( + reduced.constancyLevel, reduced.constancyType, s"(${reduced.possibleStrings})" + ) + } + } + + /** + * @return Returns a [[StringConstancyInformation]] element that corresponds to the lower bound + * from a lattice-based point of view. + */ + def lb: StringConstancyInformation = StringConstancyInformation( + StringConstancyLevel.DYNAMIC, + StringConstancyType.APPEND, + StringConstancyInformation.UnknownWordSymbol + ) + + /** + * @return Returns the / a neutral [[StringConstancyInformation]] element, i.e., an element for + * which [[StringConstancyInformation.isTheNeutralElement]] is `true`. + */ + def getNeutralElement: StringConstancyInformation = + StringConstancyInformation(StringConstancyLevel.CONSTANT) + + /** + * @return Returns a [[StringConstancyInformation]] element to indicate a `null` value. + */ + def getNullElement: StringConstancyInformation = + StringConstancyInformation(StringConstancyLevel.CONSTANT, possibleStrings = NullStringValue) + +} diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyLevel.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyLevel.scala new file mode 100644 index 0000000000..289b52c23c --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyLevel.scala @@ -0,0 +1,76 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.br.fpcf.properties.string_definition + +/** + * Values in this enumeration represent the granularity of used strings. + * + * @author Patrick Mell + */ +object StringConstancyLevel extends Enumeration { + + type StringConstancyLevel = StringConstancyLevel.Value + + /** + * This level indicates that a string has a constant value at a given read operation. + */ + final val CONSTANT = Value("constant") + + /** + * This level indicates that a string is partially constant (constant + dynamic part) at some + * read operation, that is, the initial value of a string variable needs to be preserved. For + * instance, it is fine if a string variable is modified after its initialization by + * appending another string, s2. Later, s2 might be removed partially or entirely without + * violating the constraints of this level. + */ + final val PARTIALLY_CONSTANT = Value("partially_constant") + + /** + * This level indicates that a string at some read operations has an unpredictable value. + */ + final val DYNAMIC = Value("dynamic") + + /** + * Returns the more general StringConstancyLevel of the two given levels. DYNAMIC is more + * general than PARTIALLY_CONSTANT which is more general than CONSTANT. + * + * @param level1 The first level. + * @param level2 The second level. + * @return Returns the more general level of both given inputs. + */ + def determineMoreGeneral( + level1: StringConstancyLevel, level2: StringConstancyLevel + ): StringConstancyLevel = { + if (level1 == DYNAMIC || level2 == DYNAMIC) { + DYNAMIC + } else if (level1 == PARTIALLY_CONSTANT || level2 == PARTIALLY_CONSTANT) { + PARTIALLY_CONSTANT + } else { + CONSTANT + } + } + + /** + * Returns the StringConstancyLevel of a concatenation of two values. + * CONSTANT + CONSTANT = CONSTANT + * DYNAMIC + DYNAMIC = DYNAMIC + * CONSTANT + DYNAMIC = PARTIALLY_CONSTANT + * PARTIALLY_CONSTANT + {DYNAMIC, CONSTANT} = PARTIALLY_CONSTANT + * + * @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 == PARTIALLY_CONSTANT || level2 == PARTIALLY_CONSTANT) { + PARTIALLY_CONSTANT + } else if ((level1 == CONSTANT && level2 == DYNAMIC) || + (level1 == DYNAMIC && level2 == CONSTANT)) { + PARTIALLY_CONSTANT + } else { + level1 + } + } + +} diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyType.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyType.scala new file mode 100644 index 0000000000..3227d75e4c --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringConstancyType.scala @@ -0,0 +1,34 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.br.fpcf.properties.string_definition + +/** + * Values in this enumeration represent how a string / string container, such as [[StringBuilder]], + * are changed. + * + * @author Patrick Mell + */ +object StringConstancyType extends Enumeration { + + type StringConstancyType = StringConstancyType.Value + + /** + * This type is to be used when a string value is appended to another (and also when a certain + * value represents an initialization, as an initialization can be seen as the concatenation + * of the empty string with the init value). + */ + final val APPEND = Value("append") + + /** + * This type is to be used when a string value is reset, that is, the string is set to the empty + * string (either by manually setting the value to the empty string or using a function like + * `StringBuilder.setLength(0)`). + */ + final val RESET = Value("reset") + + /** + * This type is to be used when a string or part of a string is replaced by another string + * (e.g., when calling the `replace` method of [[StringBuilder]]). + */ + final val REPLACE = Value("replace") + +} diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringTree.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringTree.scala new file mode 100644 index 0000000000..99e73e9f15 --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/StringTree.scala @@ -0,0 +1,385 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.br.fpcf.properties.string_definition + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.br.fpcf.properties.properties.StringTree +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation.InfiniteRepetitionSymbol + +/** + * Super type for modeling nodes and leafs of [[org.opalj.br.fpcf.properties.properties.StringTree]]s. + * + * @author Patrick Mell + */ +sealed abstract class StringTreeElement(val children: ListBuffer[StringTreeElement]) { + + /** + * This is a helper function which processes the `reduce` operation for [[StringTreeOr]] and + * [[StringTreeCond]] elements (as both are processed in a very similar fashion). `children` + * denotes the children of the [[StringTreeOr]] or [[StringTreeCond]] element and `processOr` + * defines whether to process [[StringTreeOr]] or [[StringTreeCond]] (the latter in case `false` + * is passed). + */ + private def processReduceCondOrReduceOr( + children: List[StringTreeElement], processOr: Boolean = true + ): List[StringConstancyInformation] = { + val reduced = children.flatMap(reduceAcc) + val resetElement = reduced.find(_.constancyType == StringConstancyType.RESET) + val replaceElement = reduced.find(_.constancyType == StringConstancyType.REPLACE) + val appendElements = reduced.filter { _.constancyType == StringConstancyType.APPEND } + val appendSci = if (appendElements.nonEmpty) { + Some(appendElements.reduceLeft((o, n) ⇒ StringConstancyInformation( + StringConstancyLevel.determineMoreGeneral(o.constancyLevel, n.constancyLevel), + StringConstancyType.APPEND, + s"${o.possibleStrings}|${n.possibleStrings}" + ))) + } else { + None + } + val scis = ListBuffer[StringConstancyInformation]() + + if (appendSci.isDefined) { + // The only difference between a Cond and an Or is how the possible strings look like + var possibleStrings = s"${appendSci.get.possibleStrings}" + if (processOr) { + if (appendElements.tail.nonEmpty) { + possibleStrings = s"($possibleStrings)" + } + } else { + possibleStrings = s"(${appendSci.get.possibleStrings})?" + } + scis.append(StringConstancyInformation( + appendSci.get.constancyLevel, appendSci.get.constancyType, possibleStrings + )) + } + if (resetElement.isDefined) { + scis.append(resetElement.get) + } + if (replaceElement.isDefined) { + scis.append(replaceElement.get) + } + scis.toList + } + + /** + * This is a helper function which processes the `reduce` operation for [[StringTreeConcat]] + * elements. + */ + private def processReduceConcat( + children: List[StringTreeElement] + ): List[StringConstancyInformation] = { + val reducedLists = children.map(reduceAcc) + // Stores whether we deal with a flat structure or with a nested structure (in the latter + // case maxNestingLevel >= 2) + val maxNestingLevel = reducedLists.foldLeft(0) { + (max: Int, next: List[StringConstancyInformation]) ⇒ Math.max(max, next.size) + } + val scis = ListBuffer[StringConstancyInformation]() + // Stores whether the last processed element was of type RESET + var wasReset = false + + reducedLists.foreach { nextSciList ⇒ + nextSciList.foreach { nextSci ⇒ + // Add the first element only if not a reset (otherwise no new information) + if (scis.isEmpty && nextSci.constancyType != StringConstancyType.RESET) { + scis.append(nextSci) + } // No two consecutive resets (as that does not add any new information either) + else if (!wasReset || nextSci.constancyType != StringConstancyType.RESET) { + // A reset / replace marks a new starting point + val isReset = nextSci.constancyType == StringConstancyType.RESET + val isReplace = nextSci.constancyType == StringConstancyType.REPLACE + if (isReset || isReplace) { + // maxNestingLevel == 1 corresponds to n consecutive append call, i.e., + // clear everything that has been seen so far + if (maxNestingLevel == 1) { + scis.clear() + // In case of replace, add the replace element + if (isReplace) { + scis.append(nextSci) + } + } // maxNestingLevel >= 2 corresponds to a new starting point (e.g., a clear + // in a if-else construction) => Add a new element + else { + scis.append(nextSci) + } + } // Otherwise, collapse / combine with previous elements + else { + scis.zipWithIndex.foreach { + case (innerSci, index) ⇒ + val collapsed = StringConstancyInformation( + StringConstancyLevel.determineForConcat( + innerSci.constancyLevel, nextSci.constancyLevel + ), + StringConstancyType.APPEND, + innerSci.possibleStrings + nextSci.possibleStrings + ) + scis(index) = collapsed + } + } + } + // For the next iteration + wasReset = nextSci.constancyType == StringConstancyType.RESET + } + } + scis.toList + } + + /** + * Accumulator / helper function for reducing a tree. + * + * @param subtree The (sub) tree to reduce. + * @return The reduced tree in the form of a list of [[StringConstancyInformation]]. That is, if + * different [[StringConstancyType]]s occur, a single StringConstancyInformation element + * is not sufficient to describe the string approximation for this function. For + * example, a [[StringConstancyType.RESET]] marks the beginning of a new string + * alternative which results in a new list element. + */ + private def reduceAcc(subtree: StringTreeElement): List[StringConstancyInformation] = { + subtree match { + case StringTreeRepetition(c, lowerBound, upperBound) ⇒ + val times = if (lowerBound.isDefined && upperBound.isDefined) + (upperBound.get - lowerBound.get).toString else InfiniteRepetitionSymbol + val reducedAcc = reduceAcc(c) + val reduced = if (reducedAcc.nonEmpty) reducedAcc.head else + StringConstancyInformation.lb + List(StringConstancyInformation( + reduced.constancyLevel, + reduced.constancyType, + s"(${reduced.possibleStrings})$times" + )) + case StringTreeConcat(cs) ⇒ processReduceConcat(cs.toList) + case StringTreeOr(cs) ⇒ processReduceCondOrReduceOr(cs.toList) + case StringTreeCond(cs) ⇒ processReduceCondOrReduceOr(cs.toList, processOr = false) + case StringTreeConst(sci) ⇒ List(sci) + } + } + + /** + * This function removes duplicate [[StringTreeConst]]s from a given list. In this + * context, two elements are equal if their [[StringTreeConst.sci]] information are equal. + * + * @param children The children from which to remove duplicates. + * @return Returns a list of [[StringTreeElement]] with unique elements. + */ + private def removeDuplicateTreeValues( + children: ListBuffer[StringTreeElement] + ): ListBuffer[StringTreeElement] = { + val seen = mutable.Map[StringConstancyInformation, Boolean]() + val unique = ListBuffer[StringTreeElement]() + children.foreach { + case next @ StringTreeConst(sci) ⇒ + if (!seen.contains(sci)) { + seen += (sci → true) + unique.append(next) + } + case loop: StringTreeRepetition ⇒ unique.append(loop) + case concat: StringTreeConcat ⇒ unique.append(concat) + case or: StringTreeOr ⇒ unique.append(or) + case cond: StringTreeCond ⇒ unique.append(cond) + } + unique + } + + /** + * Accumulator function for simplifying a tree. + */ + private def simplifyAcc(subtree: StringTree): StringTree = { + subtree match { + case StringTreeOr(cs) ⇒ + cs.foreach { + case nextC @ StringTreeOr(subChildren) ⇒ + simplifyAcc(nextC) + var insertIndex = subtree.children.indexOf(nextC) + subChildren.foreach { next ⇒ + subtree.children.insert(insertIndex, next) + insertIndex += 1 + } + subtree.children.-=(nextC) + case _ ⇒ + } + val unique = removeDuplicateTreeValues(cs) + subtree.children.clear() + subtree.children.appendAll(unique) + subtree + case stc: StringTreeCond ⇒ + // If the child of a StringTreeCond is a StringTreeRepetition, replace the + // StringTreeCond by the StringTreeRepetition element (otherwise, regular + // expressions like ((s)*)+ will follow which is equivalent to (s)*). + if (stc.children.nonEmpty && stc.children.head.isInstanceOf[StringTreeRepetition]) { + stc.children.head + } else { + stc + } + // Remaining cases are trivial + case str: StringTreeRepetition ⇒ StringTreeRepetition(simplifyAcc(str.child)) + case stc: StringTreeConcat ⇒ StringTreeConcat(stc.children.map(simplifyAcc)) + case stc: StringTreeConst ⇒ stc + } + } + + /** + * Accumulator function for grouping repetition elements. + */ + private def groupRepetitionElementsAcc(subtree: StringTree): StringTree = { + /** + * Function for processing [[StringTreeOr]] or [[StringTreeConcat]] elements as these cases + * are equal (except for distinguishing the object to return). Thus, make sure that only + * instance of these classes are passed. Otherwise, an exception will be thrown! + */ + def processConcatOrOrCase(subtree: StringTree): StringTree = { + if (!subtree.isInstanceOf[StringTreeOr] && !subtree.isInstanceOf[StringTreeConcat]) { + throw new IllegalArgumentException( + "can only process instances of StringTreeOr and StringTreeConcat" + ) + } + + var newChildren = subtree.children.map(groupRepetitionElementsAcc) + val repetitionElements = newChildren.filter(_.isInstanceOf[StringTreeRepetition]) + // Nothing to do when less than two repetition elements + if (repetitionElements.length <= 1) { + // In case there is only one (new) repetition element, replace the children + subtree.children.clear() + subtree.children.append(newChildren: _*) + subtree + } else { + val childrenOfReps = repetitionElements.map( + _.asInstanceOf[StringTreeRepetition].child + ) + val newRepElement = StringTreeRepetition(StringTreeOr(childrenOfReps)) + val indexFirstChild = newChildren.indexOf(repetitionElements.head) + newChildren = newChildren.filterNot(_.isInstanceOf[StringTreeRepetition]) + newChildren.insert(indexFirstChild, newRepElement) + if (newChildren.length == 1) { + newChildren.head + } else { + if (subtree.isInstanceOf[StringTreeOr]) { + StringTreeOr(newChildren) + } else { + StringTreeConcat(newChildren) + } + } + } + } + + subtree match { + case sto: StringTreeOr ⇒ processConcatOrOrCase(sto) + case stc: StringTreeConcat ⇒ processConcatOrOrCase(stc) + case StringTreeCond(cs) ⇒ + StringTreeCond(cs.map(groupRepetitionElementsAcc)) + case StringTreeRepetition(child, _, _) ⇒ + StringTreeRepetition(groupRepetitionElementsAcc(child)) + case stc: StringTreeConst ⇒ stc + } + } + + /** + * Reduces this [[StringTree]] instance to a [[StringConstancyInformation]] object that captures + * the information stored in this tree. + * + * @param preprocess If set to true, `this` tree will be preprocess, i.e., it will be + * simplified and repetition elements be grouped. Note that pre-processing + * changes `this` instance! + * @return A [[StringConstancyInformation]] instance that flatly describes this tree. + */ + def reduce(preprocess: Boolean = false): StringConstancyInformation = { + if (preprocess) { + simplify().groupRepetitionElements() + } + // The call to reduceMultiple is necessary as reduceAcc might return a list, e.g., if a + // clear occurred + StringConstancyInformation.reduceMultiple(reduceAcc(this)) + } + + /** + * Simplifies this tree. Currently, this means that when a (sub) tree has a + * [[StringTreeCond]] as root, ''r'', and a child, ''c'' (or several children) + * which is a [[StringTreeCond]] as well, that ''c'' is attached as a direct child + * of ''r'' (the child [[StringTreeCond]] under which ''c'' was located is then + * removed safely). + * + * @return This function modifies `this` tree and returns this instance, e.g., for chaining + * commands. + * @note Applying this function changes the representation of the tree but not produce a + * semantically different tree! Executing this function prior to [[reduce()]] simplifies + * its stringified representation. + */ + def simplify(): StringTree = simplifyAcc(this) + + /** + * This function groups repetition elements that belong together. For example, an if-else block, + * which both append to a StringBuilder is modeled as a [[StringTreeOr]] with two + * [[StringTreeRepetition]] elements. Conceptually, this is not wrong, however, may create + * confusion when interpreting the tree / expression. This function finds such groupable + * children and actually groups them. + * + * @return This function modifies `this` tree and returns this instance, e.g., for chaining + * commands. + * @note Applying this function changes the representation of the tree but not produce a + * semantically different tree! + */ + def groupRepetitionElements(): StringTree = groupRepetitionElementsAcc(this) + +} + +/** + * [[StringTreeRepetition]] models repetitive elements within a [[StringTree]], such as loops + * or recursion. [[StringTreeRepetition]] are required to have a child. A tree with a + * [[StringTreeRepetition]] that has no child is regarded as an invalid tree!
+ * + * `lowerBound` and `upperBound` refer to how often the element is repeated / evaluated when run. + * It may either refer to loop bounds or how often a recursion is repeated. If either or both values + * is/are set to `None`, it cannot be determined of often the element is actually repeated. + * Otherwise, the number of repetitions is computed by `upperBound - lowerBound`. + */ +case class StringTreeRepetition( + var child: StringTreeElement, + lowerBound: Option[Int] = None, + upperBound: Option[Int] = None +) extends StringTreeElement(ListBuffer(child)) + +/** + * [[StringTreeConcat]] models the concatenation of multiple strings. For example, if it is known + * that a string is the concatenation of ''s_1'', ..., ''s_n'' (in that order), use a + * [[StringTreeConcat]] element where the first child / first element in the `children`list + * represents ''s_1'' and the last child / last element ''s_n''. + */ +case class StringTreeConcat( + override val children: ListBuffer[StringTreeElement] +) extends StringTreeElement(children) + +/** + * [[StringTreeOr]] models that a string (or part of a string) has one out of several possible + * values. For instance, if in an `if` block and its corresponding `else` block two values, ''s1'' + * and ''s2'' are appended to a [[StringBuffer]], `sb`, a [[StringTreeOr]] can be used to model that + * `sb` can contain either ''s1'' or ''s2'' (but not both at the same time!).
+ * + * In contrast to [[StringTreeCond]], [[StringTreeOr]] provides several possible values for + * a (sub) string. + */ +case class StringTreeOr( + override val children: ListBuffer[StringTreeElement] +) extends StringTreeElement(children) + +/** + * [[StringTreeCond]] is used to model that a string (or part of a string) is optional / may + * not always be present. For example, if an `if` block (and maybe a corresponding `else if` but NO + * `else`) appends to a [[StringBuilder]], a [[StringTreeCond]] is appropriate.
+ * + * In contrast to [[StringTreeOr]], [[StringTreeCond]] provides a way to express that a (sub) + * string may have (contain) a particular but not necessarily. + */ +case class StringTreeCond( + override val children: ListBuffer[StringTreeElement] +) extends StringTreeElement(children) + +/** + * [[StringTreeConst]]s are the only elements which are supposed to act as leafs within a + * [[StringTree]]. + * + * `sci` is a [[StringConstancyInformation]] instance that resulted from evaluating an + * expression and that represents part of the value(s) a string may have. + */ +case class StringTreeConst( + sci: StringConstancyInformation +) extends StringTreeElement(ListBuffer()) diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/properties.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/properties.scala new file mode 100644 index 0000000000..df9a18d40a --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string_definition/properties.scala @@ -0,0 +1,14 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.br.fpcf.properties + +import org.opalj.br.fpcf.properties.string_definition.StringTreeElement + +package object properties { + + /** + * `StringTree` is used to build trees that represent how a particular string looks and / or how + * it can looks like from a pattern point of view (thus be approximated). + */ + type StringTree = StringTreeElement + +} diff --git a/OPAL/br/src/test/scala/org/opalj/br/cfg/AbstractCFGTest.scala b/OPAL/br/src/test/scala/org/opalj/br/cfg/AbstractCFGTest.scala index 4abc62976c..8afb7b748b 100644 --- a/OPAL/br/src/test/scala/org/opalj/br/cfg/AbstractCFGTest.scala +++ b/OPAL/br/src/test/scala/org/opalj/br/cfg/AbstractCFGTest.scala @@ -5,9 +5,9 @@ package cfg import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatest.BeforeAndAfterAll - import org.opalj.io.writeAndOpen import org.opalj.br.instructions.Instruction +import org.opalj.graphs.DominatorTree /** * Helper methods to test the CFG related methods. @@ -87,11 +87,15 @@ abstract class AbstractCFGTest extends FunSpec with Matchers with BeforeAndAfter assert((code.cfJoins -- cfJoins).isEmpty) } - /** If the execution of `f` results in an exception the CFG is printed. */ + /** + * If the execution of `f` results in an exception the CFG is printed. + * In case the dominator tree is to be printed as well, provide a defined dominator tree. + */ def printCFGOnFailure( - method: Method, - code: Code, - cfg: CFG[Instruction, Code] + method: Method, + code: Code, + cfg: CFG[Instruction, Code], + domTree: Option[DominatorTree] = None )( f: ⇒ Unit )( @@ -104,6 +108,9 @@ abstract class AbstractCFGTest extends FunSpec with Matchers with BeforeAndAfter } catch { case t: Throwable ⇒ writeAndOpen(cfg.toDot, method.name+"-CFG", ".gv") + if (domTree.isDefined) { + writeAndOpen(domTree.get.toDot(), method.name+"-DomTree", ".gv") + } throw t } } diff --git a/OPAL/br/src/test/scala/org/opalj/br/cfg/DominatorTreeTest.scala b/OPAL/br/src/test/scala/org/opalj/br/cfg/DominatorTreeTest.scala new file mode 100644 index 0000000000..a6c61a341e --- /dev/null +++ b/OPAL/br/src/test/scala/org/opalj/br/cfg/DominatorTreeTest.scala @@ -0,0 +1,117 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.br.cfg + +import java.net.URL + +import org.junit.runner.RunWith +import org.opalj.br.analyses.Project +import org.opalj.br.ClassHierarchy +import org.opalj.br.ObjectType +import org.opalj.br.TestSupport.biProject +import org.opalj.br.instructions.IF_ICMPNE +import org.opalj.br.instructions.IFNE +import org.opalj.br.Code +import org.opalj.br.instructions.IFEQ +import org.opalj.br.instructions.ILOAD +import org.scalatest.junit.JUnitRunner + +/** + * Computes the dominator tree of CFGs of a couple of methods and checks their sanity. + * + * @author Patrick Mell + */ +@RunWith(classOf[JUnitRunner]) +class DominatorTreeTest extends AbstractCFGTest { + + /** + * Takes an `index` and finds the next not-null instruction within code after `index`. + * The index of that instruction is then returned. In case no instruction could be found, the + * value of `index` is returned. + */ + private def getNextNonNullInstr(index: Int, code: Code): Int = { + var foundIndex = index + var found = false + for (i ← (index + 1).to(code.instructions.length)) { + if (!found && code.instructions(i) != null) { + foundIndex = i + found = true + } + } + foundIndex + } + + describe("Sanity of dominator trees of control flow graphs") { + + val testProject: Project[URL] = biProject("controlflow.jar") + val boringTestClassFile = testProject.classFile(ObjectType("controlflow/BoringCode")).get + + implicit val testClassHierarchy: ClassHierarchy = testProject.classHierarchy + + it("the dominator tree of a CFG with no control flow should be a tree where each "+ + "instruction is strictly dominator by its previous instruction (except for the root)") { + val m = boringTestClassFile.findMethod("singleBlock").head + val code = m.body.get + val cfg = CFGFactory(code) + val domTree = cfg.dominatorTree + + printCFGOnFailure(m, code, cfg, Some(domTree)) { + domTree.immediateDominators.zipWithIndex.foreach { + case (idom, index) ⇒ + if (index == 0) { + idom should be(0) + } else { + idom should be(index - 1) + } + } + } + } + + it("in a dominator tree of a CFG with control instructions, the first instruction within "+ + "that control structure should be dominated by the controlling instruction (like "+ + "an if)") { + val m = boringTestClassFile.findMethod("conditionalTwoReturns").head + val code = m.body.get + val cfg = CFGFactory(code) + val domTree = cfg.dominatorTree + + printCFGOnFailure(m, code, cfg, Some(domTree)) { + var index = 0 + code.foreachInstruction { next ⇒ + next match { + case _: IFNE | _: IF_ICMPNE ⇒ + val next = getNextNonNullInstr(index, code) + domTree.immediateDominators(next) should be(index) + case _ ⇒ + } + index += 1 + } + } + } + + it("in a dominator tree of a CFG with an if-else right before the return, the return "+ + "should be dominated by the if check of the if-else") { + val m = boringTestClassFile.findMethod("conditionalOneReturn").head + val code = m.body.get + val cfg = CFGFactory(code) + val domTree = cfg.dominatorTree + + printCFGOnFailure(m, code, cfg, Some(domTree)) { + val loadOfReturnOption = code.instructions.reverse.find(_.isInstanceOf[ILOAD]) + loadOfReturnOption should not be loadOfReturnOption.isEmpty + + val loadOfReturn = loadOfReturnOption.get + val indexOfLoadOfReturn = code.instructions.indexOf(loadOfReturn) + val ifOfLoadOfReturn = code.instructions.reverse.zipWithIndex.find { + case (instr, i) ⇒ + i < indexOfLoadOfReturn && instr.isInstanceOf[IFEQ] + } + ifOfLoadOfReturn should not be ifOfLoadOfReturn.isEmpty + + val indexOfIf = code.instructions.indexOf(ifOfLoadOfReturn.get._1) + domTree.immediateDominators(indexOfLoadOfReturn) should be(indexOfIf) + } + } + + } + +} diff --git a/OPAL/br/src/test/scala/org/opalj/br/string_definition/StringConstancyLevelTests.scala b/OPAL/br/src/test/scala/org/opalj/br/string_definition/StringConstancyLevelTests.scala new file mode 100644 index 0000000000..ac4f3d0984 --- /dev/null +++ b/OPAL/br/src/test/scala/org/opalj/br/string_definition/StringConstancyLevelTests.scala @@ -0,0 +1,44 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.br.string_definition + +import org.scalatest.FunSuite + +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel.CONSTANT +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel.DYNAMIC +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel.PARTIALLY_CONSTANT + +/** + * Tests for [[StringConstancyLevel]] methods. + * + * @author Patrick Mell + */ +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class StringConstancyLevelTests extends FunSuite { + + test("tests that the more general string constancy level is computed correctly") { + // Trivial cases + assert(StringConstancyLevel.determineMoreGeneral(DYNAMIC, DYNAMIC) == DYNAMIC) + assert(StringConstancyLevel.determineMoreGeneral( + PARTIALLY_CONSTANT, PARTIALLY_CONSTANT + ) == PARTIALLY_CONSTANT) + assert(StringConstancyLevel.determineMoreGeneral(CONSTANT, CONSTANT) == CONSTANT) + + // Test all other cases, start with { DYNAMIC, CONSTANT } + assert(StringConstancyLevel.determineMoreGeneral(CONSTANT, DYNAMIC) == DYNAMIC) + assert(StringConstancyLevel.determineMoreGeneral(DYNAMIC, CONSTANT) == DYNAMIC) + + // { DYNAMIC, PARTIALLY_CONSTANT } + assert(StringConstancyLevel.determineMoreGeneral(PARTIALLY_CONSTANT, DYNAMIC) == DYNAMIC) + assert(StringConstancyLevel.determineMoreGeneral(DYNAMIC, PARTIALLY_CONSTANT) == DYNAMIC) + + // { PARTIALLY_CONSTANT, CONSTANT } + assert(StringConstancyLevel.determineMoreGeneral( + PARTIALLY_CONSTANT, CONSTANT + ) == PARTIALLY_CONSTANT) + assert(StringConstancyLevel.determineMoreGeneral( + CONSTANT, PARTIALLY_CONSTANT + ) == PARTIALLY_CONSTANT) + } + +} diff --git a/OPAL/common/src/main/scala/org/opalj/graphs/DominatorTree.scala b/OPAL/common/src/main/scala/org/opalj/graphs/DominatorTree.scala index e2532d912e..e5d561ff2d 100644 --- a/OPAL/common/src/main/scala/org/opalj/graphs/DominatorTree.scala +++ b/OPAL/common/src/main/scala/org/opalj/graphs/DominatorTree.scala @@ -26,6 +26,47 @@ final class DominatorTree private ( def isAugmented: Boolean = hasVirtualStartNode + /** + * Checks whether a given node is dominated by another node in this dominator tree. + * + * @param possibleDominator The index of the node which could be a dominator. + * @param toCheck The index of the node which is to be checked whether it is dominated by the + * node identified by `possibleDominator`. + * @return Returns `true` if the node identified by `toCheck` is dominated by the node + * identified by `possibleDominator`. Otherwise, false will be returned. + */ + def doesDominate( + possibleDominator: Int, toCheck: Int + ): Boolean = doesDominate(Array(possibleDominator), toCheck) + + /** + * Convenient function which checks whether at least one node of a list, `possibleDominators`, + * dominates another node, `toCheck`. Note that analogously to `doesDominate(Int, Int)`, + * `possibleDominators` and `toCheck` contain the indices of the nodes. + * + * @note One could easily simulate the behavior of this function by looping over the possible + * dominators and call `doesDominate(Int, Int)` for each. However, this function has the + * advantage that only one iteration is necessary instead of ''n'' where ''n'' is the + * number of possible dominators. + */ + def doesDominate( + possibleDominators: Array[Int], toCheck: Int + ): Boolean = { + var nextToCheck = toCheck + var pd = possibleDominators.filter(_ < nextToCheck) + + while (pd.nonEmpty) { + if (possibleDominators.contains(nextToCheck)) { + return true + } + + nextToCheck = dom(nextToCheck) + pd = pd.filter(_ <= nextToCheck) + } + + false + } + } /** diff --git a/OPAL/common/src/main/scala/org/opalj/graphs/PostDominatorTree.scala b/OPAL/common/src/main/scala/org/opalj/graphs/PostDominatorTree.scala index f61c33c687..c259e61c0f 100644 --- a/OPAL/common/src/main/scala/org/opalj/graphs/PostDominatorTree.scala +++ b/OPAL/common/src/main/scala/org/opalj/graphs/PostDominatorTree.scala @@ -4,6 +4,8 @@ package graphs import org.opalj.collection.immutable.IntTrieSet +import scala.collection.mutable.ListBuffer + /** * A representation of a post-dominator tree (see [[PostDominatorTree$#apply*]] * for details regarding the properties). @@ -35,6 +37,27 @@ final class PostDominatorTree private[graphs] ( */ def isAugmented: Boolean = hasVirtualStartNode + /** + * Checks whether ''node1'' post-dominates ''node2''. + * + * @param node1 The index of the first node. + * @param node2 The index of the second node. + * @return Returns true if the node whose index corresponds to ''node1'' post-dominates the node + * whose index corresponds to ''node2''. Otherwise false will be returned. + */ + def doesPostDominate(node1: Int, node2: Int): Boolean = { + // Get all post-dominators of node2 (including node2) + val postDominators = ListBuffer[Int](node2) + var nextPostDom = idom(node2) + while (nextPostDom != idom(nextPostDom)) { + postDominators.append(nextPostDom) + nextPostDom = idom(nextPostDom) + } + postDominators.append(nextPostDom) + + postDominators.contains(node1) + } + } /** diff --git a/OPAL/si/src/main/scala/org/opalj/fpcf/par/EPKState.scala.temp b/OPAL/si/src/main/scala/org/opalj/fpcf/par/EPKState.scala.temp new file mode 100644 index 0000000000..e6e813e148 --- /dev/null +++ b/OPAL/si/src/main/scala/org/opalj/fpcf/par/EPKState.scala.temp @@ -0,0 +1,260 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package fpcf +package par + +import java.util.concurrent.atomic.AtomicReference + +/** + * Encapsulates the state of a single entity and its property of a specific kind. + * + * @note All operations are effectively atomic operations. + */ +sealed trait EPKState { + + /** Returns the current property extension. */ + def eOptionP: SomeEOptionP + + /** Returns `true` if no property has been computed yet; `false` otherwise. */ + final def isEPK: Boolean = eOptionP.isEPK + + /** Returns `true` if this entity/property pair is not yet final. */ + def isRefinable: Boolean + + /** Returns the underlying entity. */ + final def e: Entity = eOptionP.e + + /** + * Updates the underlying `EOptionP` value. + * + * @note This function is only defined if the current `EOptionP` value is not already a + * final value. Hence, the client is required to handle (potentially) idempotent updates + * and to take care of appropriate synchronization. + */ + def update( + newEOptionP: SomeInterimEP, + c: OnUpdateContinuation, + dependees: Traversable[SomeEOptionP], + debug: Boolean + ): SomeEOptionP + + /** + * Adds the given E/PK as a depender on this E/PK instance. + * + * @note This operation is idempotent; that is, adding the same EPK multiple times has no + * special effect. + * @note Adding a depender to a FinalEPK is not supported. + */ + def addDepender(someEPK: SomeEPK): Unit + + /** + * Removes the given E/PK from the list of dependers of this EPKState. + * + * @note This method is always defined and never throws an exception for convenience purposes. + */ + def removeDepender(someEPK: SomeEPK): Unit + + def resetDependers(): Set[SomeEPK] + + def lastDependers(): Set[SomeEPK] + + /** + * Returns the current `OnUpdateComputation` or `null`, if the `OnUpdateComputation` was + * already triggered. This is an atomic operation. Additionally – in a second step – + * removes the EPK underlying the EPKState from the the dependees and clears the dependees. + * + * @note This method is always defined and never throws an exception. + */ + def clearOnUpdateComputationAndDependees(): OnUpdateContinuation + + /** + * Returns `true` if the current `EPKState` has an `OnUpdateComputation` that was not yet + * triggered. + * + * @note The returned value may have changed in the meantime; hence, this method + * can/should only be used as a hint. + */ + def hasPendingOnUpdateComputation: Boolean + + /** + * Returns `true` if and only if this EPKState has dependees. + * + * @note The set of dependees is only update when a property computation result is processed + * and there exists, w.r.t. an Entity/Property Kind pair, always at most one + * `PropertyComputationResult`. + */ + def hasDependees: Boolean + + /** + * Returns the current set of depeendes. Defined if and only if this `EPKState` is refinable. + * + * @note The set of dependees is only update when a property computation result is processed + * and there exists, w.r.t. an Entity/Property Kind pair, always at most one + * `PropertyComputationResult`. + */ + def dependees: Traversable[SomeEOptionP] + +} + +/** + * + * @param eOptionPAR An atomic reference holding the current property extension; we need to + * use an atomic reference to enable concurrent update operations as required + * by properties computed using partial results. + * The referenced `EOptionP` is never null. + * @param cAR The on update continuation function; null if triggered. + * @param dependees The dependees; never updated concurrently. + */ +final class InterimEPKState( + var eOptionP: SomeEOptionP, + val cAR: AtomicReference[OnUpdateContinuation], + @volatile var dependees: Traversable[SomeEOptionP], + var dependersAR: AtomicReference[Set[SomeEPK]] +) extends EPKState { + + assert(eOptionP.isRefinable) + + override def isRefinable: Boolean = true + + override def addDepender(someEPK: SomeEPK): Unit = { + val dependersAR = this.dependersAR + if (dependersAR == null) + return ; + + var prev, next: Set[SomeEPK] = null + do { + prev = dependersAR.get() + next = prev + someEPK + } while (!dependersAR.compareAndSet(prev, next)) + } + + override def removeDepender(someEPK: SomeEPK): Unit = { + val dependersAR = this.dependersAR + if (dependersAR == null) + return ; + + var prev, next: Set[SomeEPK] = null + do { + prev = dependersAR.get() + next = prev - someEPK + } while (!dependersAR.compareAndSet(prev, next)) + } + + override def lastDependers(): Set[SomeEPK] = { + val dependers = dependersAR.get() + dependersAR = null + dependers + } + + override def clearOnUpdateComputationAndDependees(): OnUpdateContinuation = { + val c = cAR.getAndSet(null) + dependees = Nil + c + } + + override def hasPendingOnUpdateComputation: Boolean = cAR.get() != null + + override def update( + eOptionP: SomeInterimEP, + c: OnUpdateContinuation, + dependees: Traversable[SomeEOptionP], + debug: Boolean + ): SomeEOptionP = { + val oldEOptionP = this.eOptionP + if (debug) oldEOptionP.checkIsValidPropertiesUpdate(eOptionP, dependees) + + this.eOptionP = eOptionP + + val oldOnUpdateContinuation = cAR.getAndSet(c) + assert(oldOnUpdateContinuation == null) + + assert(this.dependees.isEmpty) + this.dependees = dependees + + oldEOptionP + } + + override def hasDependees: Boolean = dependees.nonEmpty + + override def toString: String = { + "InterimEPKState("+ + s"eOptionP=${eOptionPAR.get},"+ + s","+ + s"dependees=$dependees,"+ + s"dependers=${dependersAR.get()})" + } +} + +final class FinalEPKState(override val eOptionP: SomeEOptionP) extends EPKState { + + override def isRefinable: Boolean = false + + override def update(newEOptionP: SomeInterimEP, debug: Boolean): SomeEOptionP = { + throw new UnknownError(s"the final property $eOptionP can't be updated to $newEOptionP") + } + + override def resetDependers(): Set[SomeEPK] = { + throw new UnknownError(s"the final property $eOptionP can't have dependers") + } + + override def lastDependers(): Set[SomeEPK] = { + throw new UnknownError(s"the final property $eOptionP can't have dependers") + } + + override def addDepender(epk: SomeEPK): Unit = { + throw new UnknownError(s"final properties can't have dependers") + } + + override def removeDepender(someEPK: SomeEPK): Unit = { /* There is nothing to do! */ } + + override def clearOnUpdateComputationAndDependees(): OnUpdateContinuation = { + null + } + + override def dependees: Traversable[SomeEOptionP] = { + throw new UnknownError("final properties don't have dependees") + } + + override def hasDependees: Boolean = false + + override def setOnUpdateComputationAndDependees( + c: OnUpdateContinuation, + dependees: Traversable[SomeEOptionP] + ): Unit = { + throw new UnknownError("final properties can't have \"OnUpdateContinuations\"") + } + + override def hasPendingOnUpdateComputation: Boolean = false + + override def toString: String = s"FinalEPKState(finalEP=$eOptionP)" +} + +object EPKState { + + def apply(finalEP: SomeFinalEP): EPKState = new FinalEPKState(finalEP) + + def apply(eOptionP: SomeEOptionP): EPKState = { + new InterimEPKState( + new AtomicReference[SomeEOptionP](eOptionP), + new AtomicReference[OnUpdateContinuation]( /*null*/ ), + Nil, + new AtomicReference[Set[SomeEPK]](Set.empty) + ) + } + + def apply( + eOptionP: SomeEOptionP, + c: OnUpdateContinuation, + dependees: Traversable[SomeEOptionP] + ): EPKState = { + new InterimEPKState( + new AtomicReference[SomeEOptionP](eOptionP), + new AtomicReference[OnUpdateContinuation](c), + dependees, + new AtomicReference[Set[SomeEPK]](Set.empty) + ) + } + + def unapply(epkState: EPKState): Some[SomeEOptionP] = Some(epkState.eOptionP) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/InterproceduralComputationState.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/InterproceduralComputationState.scala new file mode 100644 index 0000000000..585697b1c6 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/InterproceduralComputationState.scala @@ -0,0 +1,262 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.Property +import org.opalj.value.ValueInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.Method +import org.opalj.br.fpcf.properties.cg.Callees +import org.opalj.br.fpcf.properties.cg.Callers +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.Path +import org.opalj.tac.DUVar +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.InterproceduralInterpretationHandler +import org.opalj.tac.FunctionCall +import org.opalj.tac.VirtualFunctionCall + +/** + * This class is to be used to store state information that are required at a later point in + * time during the analysis, e.g., due to the fact that another analysis had to be triggered to + * have all required information ready for a final result. + * + * @param entity The entity for which the analysis was started with. + * @param fieldWriteThreshold See the documentation of + * [[InterproceduralStringAnalysis#fieldWriteThreshold]]. + */ +case class InterproceduralComputationState(entity: P, fieldWriteThreshold: Int = 100) { + /** + * The Three-Address Code of the entity's method + */ + var tac: TACode[TACMethodParameter, DUVar[ValueInformation]] = _ + + /** + * The interpretation handler to use for computing a final result (if possible). + */ + var iHandler: InterproceduralInterpretationHandler = _ + + /** + * The interpretation handler to use for computing intermediate results. We need two handlers + * since they have an internal state, e.g., processed def sites, which should not interfere + * each other to produce correct results. + */ + var interimIHandler: InterproceduralInterpretationHandler = _ + + /** + * The computed lean path that corresponds to the given entity + */ + var computedLeanPath: Path = _ + + /** + * Callees information regarding the declared method that corresponds to the entity's method + */ + var callees: Callees = _ + + /** + * Callers information regarding the declared method that corresponds to the entity's method + */ + var callers: Callers = _ + + /** + * If not empty, this routine can only produce an intermediate result + */ + var dependees: List[EOptionP[Entity, Property]] = List() + + /** + * A mapping from DUVar elements to the corresponding indices of the FlatPathElements + */ + val var2IndexMapping: mutable.Map[V, ListBuffer[Int]] = mutable.Map() + + /** + * A mapping from values / indices of FlatPathElements to StringConstancyInformation + */ + val fpe2sci: mutable.Map[Int, ListBuffer[StringConstancyInformation]] = mutable.Map() + + /** + * A mapping from a value / index of a FlatPathElement to StringConstancyInformation which are + * not yet final. + */ + val interimFpe2sci: mutable.Map[Int, ListBuffer[StringConstancyInformation]] = mutable.Map() + + /** + * Used by [[appendToInterimFpe2Sci]] to track for which entities a value was appended to + * [[interimFpe2sci]]. For a discussion of the necessity, see the documentation of + * [[interimFpe2sci]]. + */ + private val entity2lastInterimFpe2SciValue: mutable.Map[V, StringConstancyInformation] = + mutable.Map() + + /** + * An analysis may depend on the evaluation of its parameters. This number indicates how many + * of such dependencies are still to be computed. + */ + var parameterDependeesCount = 0 + + /** + * Indicates whether the basic setup of the string analysis is done. This value is to be set to + * `true`, when all necessary dependees and parameters are available. + */ + var isSetupCompleted = false + + /** + * It might be that the result of parameters, which have to be evaluated, is not available right + * away. Later on, when the result is available, it is necessary to map it to the right + * position; this map stores this information. The key is the entity, with which the String + * Analysis was started recursively; the value is a pair where the first value indicates the + * index of the method and the second value the position of the parameter. + */ + val paramResultPositions: mutable.Map[P, (Int, Int)] = mutable.Map() + + /** + * Parameter values of a method / function. The structure of this field is as follows: Each item + * in the outer list holds the parameters of a concrete call. A mapping from the definition + * sites of parameter (negative values) to a correct index of `params` has to be made! + */ + var params: ListBuffer[ListBuffer[StringConstancyInformation]] = ListBuffer() + + /** + * This map is used to store information regarding arguments of function calls. In case a + * function is passed as a function parameter, the result might not be available right away but + * needs to be mapped to the correct param element of [[nonFinalFunctionArgs]] when available. + * For this, this map is used. + * For further information, see [[NonFinalFunctionArgsPos]]. + */ + val nonFinalFunctionArgsPos: NonFinalFunctionArgsPos = mutable.Map() + + /** + * This map is used to actually store the interpretations of parameters passed to functions. + * For further information, see [[NonFinalFunctionArgs]]. + */ + val nonFinalFunctionArgs: mutable.Map[FunctionCall[V], NonFinalFunctionArgs] = mutable.Map() + + /** + * During the process of updating the [[nonFinalFunctionArgs]] map, it is necessary to find out + * to which function an entity belongs. We use the following map to do this in constant time. + */ + val entity2Function: mutable.Map[P, ListBuffer[FunctionCall[V]]] = mutable.Map() + + /** + * A mapping from a method to definition sites which indicates that a method is still prepared, + * e.g., the TAC is still to be retrieved, and the list values indicate the defintion sites + * which depend on the preparations. + */ + val methodPrep2defSite: mutable.Map[Method, ListBuffer[Int]] = mutable.Map() + + /** + * A mapping which indicates whether a virtual function call is fully prepared. + */ + val isVFCFullyPrepared: mutable.Map[VirtualFunctionCall[V], Boolean] = mutable.Map() + + /** + * Takes a definition site as well as [[StringConstancyInformation]] and extends the [[fpe2sci]] + * map accordingly, however, only if `defSite` is not yet present and `sci` not present within + * the list of `defSite`. + */ + def appendToFpe2Sci( + defSite: Int, sci: StringConstancyInformation, reset: Boolean = false + ): Unit = { + if (reset || !fpe2sci.contains(defSite)) { + fpe2sci(defSite) = ListBuffer() + } + if (!fpe2sci(defSite).contains(sci)) { + fpe2sci(defSite).append(sci) + } + } + + /** + * Appends a [[StringConstancyInformation]] element to [[interimFpe2sci]]. The rules for + * appending are as follows: + *

    + *
  • If no element has been added to the interim result list belonging to `defSite`, the + * element is guaranteed to be added.
  • + *
  • If no entity is given, i.e., `None`, and the list at `defSite` does not contain + * `sci`, `sci` is guaranteed to be added. If necessary, the oldest element in the list + * belonging to `defSite` is removed.
  • + *
  • If a non-empty entity is given, it is checked whether an entry for that element has + * been added before by making use of [[entity2lastInterimFpe2SciValue]]. If so, the list is + * updated only if that element equals [[StringConstancyInformation.lb]]. The reason being + * is that otherwise the result of updating the upper bound might always produce a new + * result which would not make the analysis terminate. Basically, it might happen that the + * analysis produces for an entity ''e_1'' the result "(e1|e2)" which the analysis of + * entity ''e_2'' uses to update its state to "((e1|e2)|e3)". The analysis of ''e_1'', which + * depends on ''e_2'' and vice versa, will update its state producing "((e1|e2)|e3)" which + * makes the analysis of ''e_2'' update its to (((e1|e2)|e3)|e3) and so on.
  • + *
+ * + * @param defSite The definition site to which append the given `sci` element for. + * @param sci The [[StringConstancyInformation]] to add to the list of interim results for the + * given definition site. + * @param entity Optional. The entity for which the `sci` element was computed. + */ + def appendToInterimFpe2Sci( + defSite: Int, sci: StringConstancyInformation, entity: Option[V] = None + ): Unit = { + val numElements = var2IndexMapping.values.flatten.count(_ == defSite) + var addedNewList = false + if (!interimFpe2sci.contains(defSite)) { + interimFpe2sci(defSite) = ListBuffer() + addedNewList = true + } + // Append an element + val containsSci = interimFpe2sci(defSite).contains(sci) + if (!containsSci && entity.isEmpty) { + if (!addedNewList && interimFpe2sci(defSite).length == numElements) { + interimFpe2sci(defSite).remove(0) + } + interimFpe2sci(defSite).append(sci) + } else if (!containsSci && entity.nonEmpty) { + if (!entity2lastInterimFpe2SciValue.contains(entity.get) || + entity2lastInterimFpe2SciValue(entity.get) == StringConstancyInformation.lb) { + entity2lastInterimFpe2SciValue(entity.get) = sci + if (interimFpe2sci(defSite).nonEmpty) { + interimFpe2sci(defSite).remove(0) + } + interimFpe2sci(defSite).append(sci) + } + } + } + + /** + * Takes an entity as well as a definition site and append it to [[var2IndexMapping]]. + */ + def appendToVar2IndexMapping(entity: V, defSite: Int): Unit = { + if (!var2IndexMapping.contains(entity)) { + var2IndexMapping(entity) = ListBuffer() + } + var2IndexMapping(entity).append(defSite) + } + + /** + * Takes a TAC EPS as well as a definition site and append it to [[methodPrep2defSite]]. + */ + def appendToMethodPrep2defSite(m: Method, defSite: Int): Unit = { + if (!methodPrep2defSite.contains(m)) { + methodPrep2defSite(m) = ListBuffer() + } + if (!methodPrep2defSite(m).contains(defSite)) { + methodPrep2defSite(m).append(defSite) + } + } + + /** + * Removed the given definition site for the given method from [[methodPrep2defSite]]. If the + * entry for `m` in `methodPrep2defSite` is empty, the entry for `m` is removed. + */ + def removeFromMethodPrep2defSite(m: Method, defSite: Int): Unit = { + if (methodPrep2defSite.contains(m)) { + val index = methodPrep2defSite(m).indexOf(defSite) + if (index > -1) { + methodPrep2defSite(m).remove(index) + } + if (methodPrep2defSite(m).isEmpty) { + methodPrep2defSite.remove(m) + } + } + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/InterproceduralStringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/InterproceduralStringAnalysis.scala new file mode 100644 index 0000000000..7cb32bcc74 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/InterproceduralStringAnalysis.scala @@ -0,0 +1,932 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.FinalP +import org.opalj.fpcf.InterimLUBP +import org.opalj.fpcf.InterimResult +import org.opalj.fpcf.ProperPropertyComputationResult +import org.opalj.fpcf.Property +import org.opalj.fpcf.PropertyBounds +import org.opalj.fpcf.PropertyStore +import org.opalj.fpcf.Result +import org.opalj.fpcf.SomeEPS +import org.opalj.value.ValueInformation +import org.opalj.br.analyses.DeclaredMethodsKey +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.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.FieldType +import org.opalj.br.analyses.FieldAccessInformationKey +import org.opalj.br.fpcf.properties.cg.Callees +import org.opalj.br.fpcf.properties.cg.Callers +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.tac.Stmt +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.AbstractPathFinder +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.NestedPathElement +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.Path +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.PathTransformer +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.WindowPathFinder +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.ExprStmt +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.InterproceduralInterpretationHandler +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.FlatPathElement +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.SubPath +import org.opalj.tac.Assignment +import org.opalj.tac.DUVar +import org.opalj.tac.FunctionCall +import org.opalj.tac.MethodCall +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.NestedPathType +import org.opalj.tac.ArrayLoad +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.ArrayLoadPreparer +import org.opalj.tac.BinaryExpr +import org.opalj.tac.Expr + +/** + * InterproceduralStringAnalysis processes a read operation of a string variable at a program + * position, ''pp'', in a way that it finds the set of possible strings that can be read at ''pp''. + *

+ * In comparison to [[IntraproceduralStringAnalysis]], this version tries to resolve method calls + * that are involved in a string construction as far as possible. + *

+ * The main difference in the intra- and interprocedural implementation is the following (see the + * description of [[IntraproceduralStringAnalysis]] for a general overview): This analysis can only + * start to transform the computed lean paths into a string tree (again using a [[PathTransformer]]) + * after all relevant string values (determined by the [[InterproceduralInterpretationHandler]]) + * have been figured out. As the [[PropertyStore]] is used for recursively starting this analysis + * to determine possible strings of called method and functions, the path transformation can take + * place after all results for sub-expressions are available. Thus, the interprocedural + * interpretation handler cannot determine final results, e.g., for the array interpreter or static + * function call interpreter. This analysis handles this circumstance by first collecting all + * information for all definition sites. Only when these are available, further information, e.g., + * for the final results of arrays or static function calls, are derived. Finally, after all + * these information are ready as well, the path transformation takes place by only looking up what + * string expression corresponds to which definition sites (remember, at this point, for all + * definition sites all possible string values are known, thus look-ups are enough and no further + * interpretation is required). + * + * @author Patrick Mell + */ +class InterproceduralStringAnalysis( + val project: SomeProject +) extends FPCFAnalysis { + + // TODO: Is it possible to make the following two parameters configurable from the outside? + + /** + * To analyze an expression within a method ''m'', callers information might be necessary, e.g., + * to know with which arguments ''m'' is called. [[callersThreshold]] determines the threshold + * up to which number of callers parameter information are gathered. For "number of callers + * greater than [[callersThreshold]]", parameters are approximated with the lower bound. + */ + private val callersThreshold = 10 + + /** + * To analyze a read operation of field, ''f'', all write accesses, ''wa_f'', to ''f'' have to + * be analyzed. ''fieldWriteThreshold'' determines the threshold of ''|wa_f|'' when ''f'' is to + * be approximated as the lower bound, i.e., ''|wa_f|'' is greater than ''fieldWriteThreshold'' + * then the read operation of ''f'' is approximated as the lower bound. Otherwise, if ''|wa_f|'' + * is less or equal than ''fieldWriteThreshold'', analyze all ''wa_f'' to approximate the read + * of ''f''. + */ + private val fieldWriteThreshold = 100 + private val declaredMethods = project.get(DeclaredMethodsKey) + private final val fieldAccessInformation = project.get(FieldAccessInformationKey) + + /** + * Returns the current interim result for the given state. If required, custom lower and upper + * bounds can be used for the interim result. + */ + private def getInterimResult( + state: InterproceduralComputationState + ): InterimResult[StringConstancyProperty] = InterimResult( + state.entity, + computeNewLowerBound(state), + computeNewUpperBound(state), + state.dependees, + continuation(state) + ) + + private def computeNewUpperBound( + state: InterproceduralComputationState + ): StringConstancyProperty = { + if (state.computedLeanPath != null) { + StringConstancyProperty(new PathTransformer(state.interimIHandler).pathToStringTree( + state.computedLeanPath, state.interimFpe2sci + ).reduce(true)) + } else { + StringConstancyProperty.lb + } + } + + private def computeNewLowerBound( + state: InterproceduralComputationState + ): StringConstancyProperty = StringConstancyProperty.lb + + def analyze(data: P): ProperPropertyComputationResult = { + val state = InterproceduralComputationState(data, fieldWriteThreshold) + val dm = declaredMethods(data._2) + + val tacaiEOptP = ps(data._2, TACAI.key) + if (tacaiEOptP.hasUBP) { + if (tacaiEOptP.ub.tac.isEmpty) { + // No TAC available, e.g., because the method has no body + return Result(state.entity, StringConstancyProperty.lb) + } else { + state.tac = tacaiEOptP.ub.tac.get + } + } else { + state.dependees = tacaiEOptP :: state.dependees + } + + val calleesEOptP = ps(dm, Callees.key) + if (calleesEOptP.hasUBP) { + state.callees = calleesEOptP.ub + determinePossibleStrings(state) + } else { + state.dependees = calleesEOptP :: state.dependees + getInterimResult(state) + } + } + + /** + * Takes the `data` an analysis was started with as well as a computation `state` and determines + * the possible string values. This method returns either a final [[Result]] or an + * [[InterimResult]] depending on whether other information needs to be computed first. + */ + private def determinePossibleStrings( + state: InterproceduralComputationState + ): ProperPropertyComputationResult = { + val uvar = state.entity._1 + val defSites = uvar.definedBy.toArray.sorted + val stmts = state.tac.stmts + + if (state.tac == null || state.callees == null) { + return getInterimResult(state) + } + + if (state.computedLeanPath == null) { + state.computedLeanPath = computeLeanPath(uvar, state.tac) + } + + if (state.iHandler == null) { + state.iHandler = InterproceduralInterpretationHandler( + state.tac, ps, declaredMethods, fieldAccessInformation, state + ) + val interimState = state.copy() + interimState.tac = state.tac + interimState.computedLeanPath = state.computedLeanPath + interimState.callees = state.callees + interimState.callers = state.callers + interimState.params = state.params + state.interimIHandler = InterproceduralInterpretationHandler( + state.tac, ps, declaredMethods, fieldAccessInformation, interimState + ) + } + + var requiresCallersInfo = false + if (state.params.isEmpty) { + state.params = InterproceduralStringAnalysis.getParams(state.entity) + } + if (state.params.isEmpty) { + // In case a parameter is required for approximating a string, retrieve callers information + // (but only once and only if the expressions is not a local string) + val hasCallersOrParamInfo = state.callers == null && state.params.isEmpty + requiresCallersInfo = if (defSites.exists(_ < 0)) { + if (InterpretationHandler.isStringConstExpression(uvar)) { + hasCallersOrParamInfo + } else if (InterproceduralStringAnalysis.isSupportedPrimitiveNumberType(uvar)) { + val numType = uvar.value.asPrimitiveValue.primitiveType.toJava + val sci = InterproceduralStringAnalysis. + getDynamicStringInformationForNumberType(numType) + return Result(state.entity, StringConstancyProperty(sci)) + } else { + // StringBuilders as parameters are currently not evaluated + return Result(state.entity, StringConstancyProperty.lb) + } + } else { + val call = stmts(defSites.head).asAssignment.expr + if (InterpretationHandler.isStringBuilderBufferToStringCall(call)) { + val (_, hasInitDefSites) = computeLeanPathForStringBuilder(uvar, state.tac) + if (!hasInitDefSites) { + return Result(state.entity, StringConstancyProperty.lb) + } + val hasSupportedParamType = state.entity._2.parameterTypes.exists { + InterproceduralStringAnalysis.isSupportedType + } + if (hasSupportedParamType) { + hasParamUsageAlongPath(state.computedLeanPath, state.tac.stmts) + } else { + !hasCallersOrParamInfo + } + } else { + !hasCallersOrParamInfo + } + } + } + + if (requiresCallersInfo) { + val dm = declaredMethods(state.entity._2) + val callersEOptP = ps(dm, Callers.key) + if (callersEOptP.hasUBP) { + state.callers = callersEOptP.ub + if (!registerParams(state)) { + return getInterimResult(state) + } + } else { + state.dependees = callersEOptP :: state.dependees + return getInterimResult(state) + } + } + + if (state.parameterDependeesCount > 0) { + return getInterimResult(state) + } else { + state.isSetupCompleted = true + } + + // sci stores the final StringConstancyInformation (if it can be determined now at all) + var sci = StringConstancyProperty.lb.stringConstancyInformation + // Interpret a function / method parameter using the parameter information in state + if (defSites.head < 0) { + val r = state.iHandler.processDefSite(defSites.head, state.params.toList) + val sci = r.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + return Result(state.entity, StringConstancyProperty(sci)) + } + + val call = stmts(defSites.head).asAssignment.expr + var attemptFinalResultComputation = false + if (InterpretationHandler.isStringBuilderBufferToStringCall(call)) { + // Find DUVars, that the analysis of the current entity depends on + val dependentVars = findDependentVars(state.computedLeanPath, stmts, uvar) + if (dependentVars.nonEmpty) { + dependentVars.keys.foreach { nextVar ⇒ + val toAnalyze = (nextVar, state.entity._2) + dependentVars.foreach { case (k, v) ⇒ state.appendToVar2IndexMapping(k, v) } + val ep = propertyStore(toAnalyze, StringConstancyProperty.key) + ep match { + case FinalP(p) ⇒ return processFinalP(state, ep.e, p) + case _ ⇒ state.dependees = ep :: state.dependees + } + } + } else { + attemptFinalResultComputation = true + } + } // If not a call to String{Builder, Buffer}.toString, then we deal with pure strings + else { + attemptFinalResultComputation = true + } + + if (attemptFinalResultComputation) { + if (state.dependees.isEmpty && computeResultsForPath(state.computedLeanPath, state)) { + // Check whether we deal with the empty string; it requires special treatment as the + // PathTransformer#pathToStringTree would not handle it correctly (as + // PathTransformer#pathToStringTree is involved in a mutual recursion) + val isEmptyString = if (state.computedLeanPath.elements.length == 1) { + state.computedLeanPath.elements.head match { + case FlatPathElement(i) ⇒ + state.fpe2sci.contains(i) && state.fpe2sci(i).length == 1 && + state.fpe2sci(i).head == StringConstancyInformation.getNeutralElement + case _ ⇒ false + } + } else false + + sci = if (isEmptyString) { + StringConstancyInformation.getNeutralElement + } else { + new PathTransformer(state.iHandler).pathToStringTree( + state.computedLeanPath, state.fpe2sci + ).reduce(true) + } + } + } + + if (state.dependees.nonEmpty) { + getInterimResult(state) + } else { + InterproceduralStringAnalysis.unregisterParams(state.entity) + Result(state.entity, StringConstancyProperty(sci)) + } + } + + /** + * Continuation function for this analysis. + * + * @param state The current computation state. Within this continuation, dependees of the state + * might be updated. Furthermore, methods processing this continuation might alter + * the state. + * @return Returns a final result if (already) available. Otherwise, an intermediate result will + * be returned. + */ + private def continuation( + state: InterproceduralComputationState + )(eps: SomeEPS): ProperPropertyComputationResult = { + state.dependees = state.dependees.filter(_.e != eps.e) + eps.pk match { + case TACAI.key ⇒ eps match { + case FinalP(tac: TACAI) ⇒ + // Set the TAC only once (the TAC might be requested for other methods, so this + // makes sure we do not overwrite the state's TAC) + if (state.tac == null) { + state.tac = tac.tac.get + } + determinePossibleStrings(state) + case _ ⇒ + state.dependees = eps :: state.dependees + getInterimResult(state) + } + case Callees.key ⇒ eps match { + case FinalP(callees: Callees) ⇒ + state.callees = callees + if (state.dependees.isEmpty) { + determinePossibleStrings(state) + } else { + getInterimResult(state) + } + case _ ⇒ + state.dependees = eps :: state.dependees + getInterimResult(state) + } + case Callers.key ⇒ eps match { + case FinalP(callers: Callers) ⇒ + state.callers = callers + if (state.dependees.isEmpty) { + registerParams(state) + determinePossibleStrings(state) + } else { + getInterimResult(state) + } + case _ ⇒ + state.dependees = eps :: state.dependees + getInterimResult(state) + } + case StringConstancyProperty.key ⇒ + eps match { + case FinalEP(entity, p: StringConstancyProperty) ⇒ + val e = entity.asInstanceOf[P] + // For updating the interim state + state.var2IndexMapping(eps.e.asInstanceOf[P]._1).foreach { i ⇒ + state.appendToInterimFpe2Sci(i, p.stringConstancyInformation) + } + // If necessary, update the parameter information with which the + // surrounding function / method of the entity was called with + if (state.paramResultPositions.contains(e)) { + val pos = state.paramResultPositions(e) + state.params(pos._1)(pos._2) = p.stringConstancyInformation + state.paramResultPositions.remove(e) + state.parameterDependeesCount -= 1 + } + + // If necessary, update parameter information of function calls + if (state.entity2Function.contains(e)) { + state.var2IndexMapping(e._1).foreach(state.appendToFpe2Sci( + _, p.stringConstancyInformation + )) + // Update the state + state.entity2Function(e).foreach { f ⇒ + val pos = state.nonFinalFunctionArgsPos(f)(e) + val finalEp = FinalEP(e, p) + state.nonFinalFunctionArgs(f)(pos._1)(pos._2)(pos._3) = finalEp + // Housekeeping + val index = state.entity2Function(e).indexOf(f) + state.entity2Function(e).remove(index) + if (state.entity2Function(e).isEmpty) { + state.entity2Function.remove(e) + } + } + // Continue only after all necessary function parameters are evaluated + if (state.entity2Function.nonEmpty) { + return getInterimResult(state) + } else { + // We could try to determine a final result before all function + // parameter information are available, however, this will + // definitely result in finding some intermediate result. Thus, + // defer this computations when we know that all necessary + // information are available + state.entity2Function.clear() + if (!computeResultsForPath(state.computedLeanPath, state)) { + return determinePossibleStrings(state) + } + } + } + + if (state.isSetupCompleted && state.parameterDependeesCount == 0) { + processFinalP(state, eps.e, p) + } else { + determinePossibleStrings(state) + } + case InterimLUBP(_: StringConstancyProperty, ub: StringConstancyProperty) ⇒ + state.dependees = eps :: state.dependees + val uvar = eps.e.asInstanceOf[P]._1 + state.var2IndexMapping(uvar).foreach { i ⇒ + state.appendToInterimFpe2Sci( + i, ub.stringConstancyInformation, Some(uvar) + ) + } + getInterimResult(state) + case _ ⇒ + state.dependees = eps :: state.dependees + getInterimResult(state) + } + } + } + + private def finalizePreparations( + path: Path, + state: InterproceduralComputationState, + iHandler: InterproceduralInterpretationHandler + ): Unit = path.elements.foreach { + case FlatPathElement(index) ⇒ + if (!state.fpe2sci.contains(index)) { + iHandler.finalizeDefSite(index, state) + } + case npe: NestedPathElement ⇒ + finalizePreparations(Path(npe.element.toList), state, iHandler) + case _ ⇒ + } + + /** + * computeFinalResult computes the final result of an analysis. This includes the computation + * of instruction that could only be prepared (e.g., if an array load included a method call, + * its final result is not yet ready, however, this function finalizes, e.g., that load). + * + * @param state The final computation state. For this state the following criteria must apply: + * For each [[FlatPathElement]], there must be a corresponding entry in + * `state.fpe2sci`. If this criteria is not met, a [[NullPointerException]] will + * be thrown (in this case there was some work to do left and this method should + * not have been called)! + * @return Returns the final result. + */ + private def computeFinalResult(state: InterproceduralComputationState): Result = { + finalizePreparations(state.computedLeanPath, state, state.iHandler) + val finalSci = new PathTransformer(state.iHandler).pathToStringTree( + state.computedLeanPath, state.fpe2sci, resetExprHandler = false + ).reduce(true) + InterproceduralStringAnalysis.unregisterParams(state.entity) + Result(state.entity, StringConstancyProperty(finalSci)) + } + + /** + * `processFinalP` is responsible for handling the case that the `propertyStore` outputs a + * [[org.opalj.fpcf.FinalP]]. + */ + private def processFinalP( + state: InterproceduralComputationState, + e: Entity, + p: Property + ): ProperPropertyComputationResult = { + // Add mapping information (which will be used for computing the final result) + val retrievedProperty = p.asInstanceOf[StringConstancyProperty] + val currentSci = retrievedProperty.stringConstancyInformation + state.var2IndexMapping(e.asInstanceOf[P]._1).foreach { + state.appendToFpe2Sci(_, currentSci) + } + + state.dependees = state.dependees.filter(_.e != e) + // No more dependees => Return the result for this analysis run + if (state.dependees.isEmpty) { + computeFinalResult(state) + } else { + getInterimResult(state) + } + } + + /** + * This method takes a computation state, `state` as well as a TAC provider, `tacProvider`, and + * determines the interpretations of all parameters of the method under analysis. These + * interpretations are registered using [[InterproceduralStringAnalysis.registerParams]]. + * The return value of this function indicates whether a the parameter evaluation is done + * (`true`) or not yet (`false`). + */ + private def registerParams( + state: InterproceduralComputationState + ): Boolean = { + val callers = state.callers.callers(declaredMethods).toSeq + if (callers.length > callersThreshold) { + state.params.append( + state.entity._2.parameterTypes.map { + _: FieldType ⇒ StringConstancyInformation.lb + }.to[ListBuffer] + ) + return false + } + + var hasIntermediateResult = false + callers.zipWithIndex.foreach { + case ((m, pc, _), methodIndex) ⇒ + val tac = propertyStore(m.definedMethod, TACAI.key).ub.tac.get + val params = tac.stmts(tac.pcToIndex(pc)) match { + case Assignment(_, _, fc: FunctionCall[V]) ⇒ fc.params + case Assignment(_, _, mc: MethodCall[V]) ⇒ mc.params + case ExprStmt(_, fc: FunctionCall[V]) ⇒ fc.params + case ExprStmt(_, fc: MethodCall[V]) ⇒ fc.params + case mc: MethodCall[V] ⇒ mc.params + case _ ⇒ List() + } + params.zipWithIndex.foreach { + case (p, paramIndex) ⇒ + // Add an element to the params list (we do it here because we know how many + // parameters there are) + if (state.params.length <= methodIndex) { + state.params.append(params.indices.map(_ ⇒ + StringConstancyInformation.getNeutralElement).to[ListBuffer]) + } + // Recursively analyze supported types + if (InterproceduralStringAnalysis.isSupportedType(p.asVar)) { + val paramEntity = (p.asVar, m.definedMethod) + val eps = propertyStore(paramEntity, StringConstancyProperty.key) + state.appendToVar2IndexMapping(paramEntity._1, paramIndex) + eps match { + case FinalP(r) ⇒ + state.params(methodIndex)(paramIndex) = r.stringConstancyInformation + case _ ⇒ + state.dependees = eps :: state.dependees + hasIntermediateResult = true + state.paramResultPositions(paramEntity) = (methodIndex, paramIndex) + state.parameterDependeesCount += 1 + } + } else { + state.params(methodIndex)(paramIndex) = + StringConstancyProperty.lb.stringConstancyInformation + } + + } + } + // If all parameters could already be determined, register them + if (!hasIntermediateResult) { + InterproceduralStringAnalysis.registerParams(state.entity, state.params) + } + !hasIntermediateResult + } + + /** + * This function traverses the given path, computes all string values along the path and stores + * these information in the given state. + * + * @param p The path to traverse. + * @param state The current state of the computation. This function will alter + * [[InterproceduralComputationState.fpe2sci]]. + * @return Returns `true` if all values computed for the path are final results. + */ + private def computeResultsForPath( + p: Path, + state: InterproceduralComputationState + ): Boolean = { + var hasFinalResult = true + + p.elements.foreach { + case FlatPathElement(index) ⇒ + if (!state.fpe2sci.contains(index)) { + val eOptP = state.iHandler.processDefSite(index, state.params.toList) + if (eOptP.isFinal) { + val p = eOptP.asFinal.p.asInstanceOf[StringConstancyProperty] + state.appendToFpe2Sci(index, p.stringConstancyInformation, reset = true) + } else { + hasFinalResult = false + } + } + case npe: NestedPathElement ⇒ + val subFinalResult = computeResultsForPath( + Path(npe.element.toList), state + ) + if (hasFinalResult) { + hasFinalResult = subFinalResult + } + case _ ⇒ + } + + hasFinalResult + } + + /** + * This function is a wrapper function for [[computeLeanPathForStringConst]] and + * [[computeLeanPathForStringBuilder]]. + */ + private def computeLeanPath( + duvar: V, tac: TACode[TACMethodParameter, DUVar[ValueInformation]] + ): Path = { + val defSites = duvar.definedBy.toArray.sorted + if (defSites.head < 0) { + computeLeanPathForStringConst(duvar) + } else { + val call = tac.stmts(defSites.head).asAssignment.expr + if (InterpretationHandler.isStringBuilderBufferToStringCall(call)) { + val (leanPath, _) = computeLeanPathForStringBuilder(duvar, tac) + leanPath + } else { + computeLeanPathForStringConst(duvar) + } + } + } + + /** + * This function computes the lean path for a [[DUVar]] which is required to be a string + * expressions. + */ + private def computeLeanPathForStringConst(duvar: V): Path = { + val defSites = duvar.definedBy.toArray.sorted + if (defSites.length == 1) { + // Trivial case for just one element + Path(List(FlatPathElement(defSites.head))) + } else { + // For > 1 definition sites, create a nest path element with |defSites| many + // children where each child is a NestPathElement(FlatPathElement) + val children = ListBuffer[SubPath]() + defSites.foreach { ds ⇒ + children.append(NestedPathElement(ListBuffer(FlatPathElement(ds)), None)) + } + Path(List(NestedPathElement(children, Some(NestedPathType.CondWithAlternative)))) + } + } + + /** + * This function computes the lean path for a [[DUVar]] which is required to stem from a + * `String{Builder, Buffer}#toString()` call. For this, the `tac` of the method, in which + * `duvar` resides, is required. + * This function then returns a pair of values: The first value is the computed lean path and + * the second value indicates whether the String{Builder, Buffer} has initialization sites + * within the method stored in `tac`. If it has no initialization sites, it returns + * `(null, false)` and otherwise `(computed lean path, true)`. + */ + private def computeLeanPathForStringBuilder( + duvar: V, tac: TACode[TACMethodParameter, DUVar[ValueInformation]] + ): (Path, Boolean) = { + val pathFinder: AbstractPathFinder = new WindowPathFinder(tac.cfg) + val initDefSites = InterpretationHandler.findDefSiteOfInit(duvar, tac.stmts) + if (initDefSites.isEmpty) { + (null, false) + } else { + val paths = pathFinder.findPaths(initDefSites, duvar.definedBy.toArray.max) + (paths.makeLeanPath(duvar, tac.stmts), true) + } + } + + private def hasParamUsageAlongPath(path: Path, stmts: Array[Stmt[V]]): Boolean = { + def hasExprParamUsage(expr: Expr[V]): Boolean = expr match { + case al: ArrayLoad[V] ⇒ + ArrayLoadPreparer.getStoreAndLoadDefSites(al, stmts).exists(_ < 0) + case duvar: V ⇒ duvar.definedBy.exists(_ < 0) + case fc: FunctionCall[V] ⇒ fc.params.exists(hasExprParamUsage) + case mc: MethodCall[V] ⇒ mc.params.exists(hasExprParamUsage) + case be: BinaryExpr[V] ⇒ hasExprParamUsage(be.left) || hasExprParamUsage(be.right) + case _ ⇒ false + } + + path.elements.exists { + case FlatPathElement(index) ⇒ stmts(index) match { + case Assignment(_, _, expr) ⇒ hasExprParamUsage(expr) + case ExprStmt(_, expr) ⇒ hasExprParamUsage(expr) + case _ ⇒ false + } + case NestedPathElement(subPath, _) ⇒ hasParamUsageAlongPath(Path(subPath.toList), stmts) + case _ ⇒ false + } + } + + /** + * Helper / accumulator function for finding dependees. For how dependees are detected, see + * findDependentVars. Returns a list of pairs of DUVar and the index of the + * FlatPathElement.element in which it occurs. + */ + private def findDependeesAcc( + subpath: SubPath, + stmts: Array[Stmt[V]], + target: V, + foundDependees: ListBuffer[(V, Int)], + hasTargetBeenSeen: Boolean + ): (ListBuffer[(V, Int)], Boolean) = { + var encounteredTarget = false + subpath match { + case fpe: FlatPathElement ⇒ + if (target.definedBy.contains(fpe.element)) { + encounteredTarget = true + } + // For FlatPathElements, search for DUVars on which the toString method is called + // and where these toString calls are the parameter of an append call + stmts(fpe.element) match { + case ExprStmt(_, outerExpr) ⇒ + if (InterpretationHandler.isStringBuilderBufferAppendCall(outerExpr)) { + val param = outerExpr.asVirtualFunctionCall.params.head.asVar + param.definedBy.filter(_ >= 0).foreach { ds ⇒ + val expr = stmts(ds).asAssignment.expr + if (InterpretationHandler.isStringBuilderBufferToStringCall(expr)) { + foundDependees.append(( + outerExpr.asVirtualFunctionCall.params.head.asVar, + fpe.element + )) + } + } + } + case _ ⇒ + } + (foundDependees, encounteredTarget) + case npe: NestedPathElement ⇒ + npe.element.foreach { nextSubpath ⇒ + if (!encounteredTarget) { + val (_, seen) = findDependeesAcc( + nextSubpath, stmts, target, foundDependees, encounteredTarget + ) + encounteredTarget = seen + } + } + (foundDependees, encounteredTarget) + case _ ⇒ (foundDependees, encounteredTarget) + } + } + + /** + * Takes a `path`, this should be the lean path of a [[Path]], as well as a context in the form + * of statements, `stmts`, and detects all dependees within `path`. Dependees are found by + * looking at all elements in the path, and check whether the argument of an `append` call is a + * value that stems from a `toString` call of a [[StringBuilder]] or [[StringBuffer]]. This + * function then returns the found UVars along with the indices of those append statements. + * + * @note In order to make sure that a [[org.opalj.tac.DUVar]] does not depend on itself, pass + * this variable as `ignore`. + */ + private def findDependentVars( + path: Path, stmts: Array[Stmt[V]], ignore: V + ): mutable.LinkedHashMap[V, Int] = { + val dependees = mutable.LinkedHashMap[V, Int]() + val ignoreNews = InterpretationHandler.findNewOfVar(ignore, stmts) + var wasTargetSeen = false + + path.elements.foreach { nextSubpath ⇒ + if (!wasTargetSeen) { + val (currentDeps, encounteredTarget) = findDependeesAcc( + nextSubpath, stmts, ignore, ListBuffer(), hasTargetBeenSeen = false + ) + wasTargetSeen = encounteredTarget + currentDeps.foreach { nextPair ⇒ + val newExpressions = InterpretationHandler.findNewOfVar(nextPair._1, stmts) + if (ignore != nextPair._1 && ignoreNews != newExpressions) { + dependees.put(nextPair._1, nextPair._2) + } + } + } + } + dependees + } + +} + +object InterproceduralStringAnalysis { + + /** + * Maps entities to a list of lists of parameters. As currently this analysis works context- + * insensitive, we have a list of lists to capture all parameters of all potential method / + * function calls. + */ + private val paramInfos = mutable.Map[Entity, ListBuffer[ListBuffer[StringConstancyInformation]]]() + + def registerParams(e: Entity, scis: ListBuffer[ListBuffer[StringConstancyInformation]]): Unit = { + if (!paramInfos.contains(e)) { + paramInfos(e) = ListBuffer(scis: _*) + } else { + paramInfos(e).appendAll(scis) + } + } + + def unregisterParams(e: Entity): Unit = paramInfos.remove(e) + + def getParams(e: Entity): ListBuffer[ListBuffer[StringConstancyInformation]] = + if (paramInfos.contains(e)) { + paramInfos(e) + } else { + ListBuffer() + } + + /** + * This function checks whether a given type is a supported primitive type. Supported currently + * means short, int, float, or double. + */ + def isSupportedPrimitiveNumberType(v: V): Boolean = { + val value = v.value + if (value.isPrimitiveValue) { + isSupportedPrimitiveNumberType(value.asPrimitiveValue.primitiveType.toJava) + } else { + false + } + } + + /** + * This function checks whether a given type is a supported primitive type. Supported currently + * means short, int, float, or double. + */ + def isSupportedPrimitiveNumberType(typeName: String): Boolean = + typeName == "short" || typeName == "int" || typeName == "float" || typeName == "double" + + /** + * Checks whether a given type, identified by its string representation, is supported by the + * string analysis. That means, if this function returns `true`, a value, which is of type + * `typeName` may be approximated by the string analysis better than just the lower bound. + * + * @param typeName The name of the type to check. May either be the name of a primitive type or + * a fully-qualified class name (dot-separated). + * @return Returns `true`, if `typeName` is an element in [char, short, int, float, double, + * java.lang.String] and `false` otherwise. + */ + def isSupportedType(typeName: String): Boolean = + typeName == "char" || isSupportedPrimitiveNumberType(typeName) || + typeName == "java.lang.String" || typeName == "java.lang.String[]" + + /** + * Determines whether a given [[V]] element ([[DUVar]]) is supported by the string analysis. + * + * @param v The element to check. + * @return Returns true if the given [[FieldType]] is of a supported type. For supported types, + * see [[InterproceduralStringAnalysis.isSupportedType(String)]]. + */ + def isSupportedType(v: V): Boolean = + if (v.value.isPrimitiveValue) { + isSupportedType(v.value.asPrimitiveValue.primitiveType.toJava) + } else { + try { + isSupportedType(v.value.verificationTypeInfo.asObjectVariableInfo.clazz.toJava) + } catch { + case _: Exception ⇒ false + } + } + + /** + * Determines whether a given [[FieldType]] element is supported by the string analysis. + * + * @param fieldType The element to check. + * @return Returns true if the given [[FieldType]] is of a supported type. For supported types, + * see [[InterproceduralStringAnalysis.isSupportedType(String)]]. + */ + def isSupportedType(fieldType: FieldType): Boolean = isSupportedType(fieldType.toJava) + + /** + * Takes the name of a primitive number type - supported types are short, int, float, double - + * and returns the dynamic [[StringConstancyInformation]] for that type. In case an unsupported + * type is given [[StringConstancyInformation.UnknownWordSymbol]] is returned as possible + * strings. + */ + def getDynamicStringInformationForNumberType( + numberType: String + ): StringConstancyInformation = { + val possibleStrings = numberType match { + case "short" | "int" ⇒ StringConstancyInformation.IntValue + case "float" | "double" ⇒ StringConstancyInformation.FloatValue + case _ ⇒ StringConstancyInformation.UnknownWordSymbol + } + StringConstancyInformation(StringConstancyLevel.DYNAMIC, possibleStrings = possibleStrings) + } + +} + +sealed trait InterproceduralStringAnalysisScheduler extends FPCFAnalysisScheduler { + + final def derivedProperty: PropertyBounds = PropertyBounds.lub(StringConstancyProperty) + + final override def uses: Set[PropertyBounds] = Set( + PropertyBounds.ub(TACAI), + PropertyBounds.ub(Callees), + PropertyBounds.lub(StringConstancyProperty) + ) + + final override type InitializationData = InterproceduralStringAnalysis + final override def init(p: SomeProject, ps: PropertyStore): InitializationData = { + new InterproceduralStringAnalysis(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 = {} + +} + +/** + * Executor for the lazy analysis. + */ +object LazyInterproceduralStringAnalysis + extends InterproceduralStringAnalysisScheduler with FPCFLazyAnalysisScheduler { + + override def register( + p: SomeProject, ps: PropertyStore, analysis: InitializationData + ): FPCFAnalysis = { + val analysis = new InterproceduralStringAnalysis(p) + ps.registerLazyPropertyComputation(StringConstancyProperty.key, analysis.analyze) + analysis + } + + override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/IntraproceduralStringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/IntraproceduralStringAnalysis.scala new file mode 100644 index 0000000000..e41ae3473a --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/IntraproceduralStringAnalysis.scala @@ -0,0 +1,369 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalP +import org.opalj.fpcf.InterimLUBP +import org.opalj.fpcf.InterimResult +import org.opalj.fpcf.ProperPropertyComputationResult +import org.opalj.fpcf.Property +import org.opalj.fpcf.PropertyBounds +import org.opalj.fpcf.PropertyStore +import org.opalj.fpcf.Result +import org.opalj.fpcf.SomeEPS +import org.opalj.value.ValueInformation +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.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.cg.Callees +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.ExprStmt +import org.opalj.tac.Stmt +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural.IntraproceduralInterpretationHandler +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.AbstractPathFinder +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.FlatPathElement +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.NestedPathElement +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.Path +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.PathTransformer +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.SubPath +import org.opalj.tac.fpcf.analyses.string_analysis.preprocessing.WindowPathFinder +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.DUVar +import org.opalj.tac.EagerDetachedTACAIKey +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode + +/** + * IntraproceduralStringAnalysis processes a read operation of a local string variable at a program + * position, ''pp'', in a way that it finds the set of possible strings that can be read at ''pp''. + *

+ * This analysis takes into account only the enclosing function as a context, i.e., it + * intraprocedural. Values coming from other functions are regarded as dynamic values even if the + * function returns a constant string value. + *

+ * From a high-level perspective, this analysis works as follows. First, it has to be differentiated + * whether string literals / variables or String{Buffer, Builder} are to be processed. + * For the former, the definition sites are processed. Only one definition site is the trivial case + * and directly corresponds to a leaf node in the string tree (such trees consist of only one node). + * Multiple definition sites indicate > 1 possible initialization values and are transformed into a + * string tree whose root node is an OR element and the children are the possible initialization + * values. Note that all this is handled by [[StringConstancyInformation.reduceMultiple]]. + *

+ * For the latter, String{Buffer, Builder}, lean paths from the definition sites to the usage + * (indicated by the given DUVar) is computed. That is, all paths from all definition sites to the + * usage where only statements are contained that include the String{Builder, Buffer} object of + * interest in some way (like an "append" or "replace" operation for example). These paths are then + * transformed into a string tree by making use of a [[PathTransformer]]. + * + * @author Patrick Mell + */ +class IntraproceduralStringAnalysis( + val project: SomeProject +) extends FPCFAnalysis { + + /** + * This class is to be used to store state information that are required at a later point in + * time during the analysis, e.g., due to the fact that another analysis had to be triggered to + * have all required information ready for a final result. + */ + private case class ComputationState( + // The lean path that was computed + computedLeanPath: Path, + // A mapping from DUVar elements to the corresponding indices of the FlatPathElements + var2IndexMapping: mutable.Map[V, Int], + // A mapping from values of FlatPathElements to StringConstancyInformation + fpe2sci: mutable.Map[Int, StringConstancyInformation], + // The three-address code of the method in which the entity under analysis resides + tac: TACode[TACMethodParameter, DUVar[ValueInformation]] + ) + + def analyze(data: P): ProperPropertyComputationResult = { + // sci stores the final StringConstancyInformation (if it can be determined now at all) + var sci = StringConstancyProperty.lb.stringConstancyInformation + + val tacProvider = p.get(EagerDetachedTACAIKey) + val tac = tacProvider(data._2) + + // Uncomment the following code to get the TAC from the property store + // val tacaiEOptP = ps(data._2, TACAI.key) + // var tac: TACode[TACMethodParameter, DUVar[ValueInformation]] = null + // if (tacaiEOptP.hasUBP) { + // if (tacaiEOptP.ub.tac.isEmpty) { + // // No TAC available, e.g., because the method has no body + // return Result(data, StringConstancyProperty.lb) + // } else { + // tac = tacaiEOptP.ub.tac.get + // } + // } + val cfg = tac.cfg + val stmts = tac.stmts + + val uvar = data._1 + val defSites = uvar.definedBy.toArray.sorted + // Function parameters are currently regarded as dynamic value; the following if finds read + // operations of strings (not String{Builder, Buffer}s, they will be handles further down + if (defSites.head < 0) { + return Result(data, StringConstancyProperty.lb) + } + val pathFinder: AbstractPathFinder = new WindowPathFinder(cfg) + + // If not empty, this very routine can only produce an intermediate result + val dependees: mutable.Map[Entity, ListBuffer[EOptionP[Entity, Property]]] = mutable.Map() + // state will be set to a non-null value if this analysis needs to call other analyses / + // itself; only in the case it calls itself, will state be used, thus, it is valid to + // initialize it with null + var state: ComputationState = null + + val call = stmts(defSites.head).asAssignment.expr + if (InterpretationHandler.isStringBuilderBufferToStringCall(call)) { + val initDefSites = InterpretationHandler.findDefSiteOfInit(uvar, stmts) + // initDefSites empty => String{Builder,Buffer} from method parameter is to be evaluated + if (initDefSites.isEmpty) { + return Result(data, StringConstancyProperty.lb) + } + + val paths = pathFinder.findPaths(initDefSites, uvar.definedBy.head) + val leanPaths = paths.makeLeanPath(uvar, stmts) + + // Find DUVars, that the analysis of the current entity depends on + val dependentVars = findDependentVars(leanPaths, stmts, uvar) + if (dependentVars.nonEmpty) { + dependentVars.keys.foreach { nextVar ⇒ + val toAnalyze = (nextVar, data._2) + val fpe2sci = mutable.Map[Int, StringConstancyInformation]() + state = ComputationState(leanPaths, dependentVars, fpe2sci, tac) + val ep = propertyStore(toAnalyze, StringConstancyProperty.key) + ep match { + case FinalP(p) ⇒ + return processFinalP(data, dependees.values.flatten, state, ep.e, p) + case _ ⇒ + if (!dependees.contains(data)) { + dependees(data) = ListBuffer() + } + dependees(data).append(ep) + } + } + } else { + val interpretationHandler = IntraproceduralInterpretationHandler(tac) + sci = new PathTransformer( + interpretationHandler + ).pathToStringTree(leanPaths).reduce(true) + } + } // If not a call to String{Builder, Buffer}.toString, then we deal with pure strings + else { + val interHandler = IntraproceduralInterpretationHandler(tac) + sci = StringConstancyInformation.reduceMultiple( + uvar.definedBy.toArray.sorted.map { ds ⇒ + val r = interHandler.processDefSite(ds).asFinal + r.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + } + ) + } + + if (dependees.nonEmpty) { + InterimResult( + data._1, + StringConstancyProperty.ub, + StringConstancyProperty.lb, + dependees.values.flatten, + continuation(data, dependees.values.flatten, state) + ) + } else { + Result(data, StringConstancyProperty(sci)) + } + } + + /** + * `processFinalP` is responsible for handling the case that the `propertyStore` outputs a + * [[FinalP]]. + */ + private def processFinalP( + data: P, + dependees: Iterable[EOptionP[Entity, Property]], + state: ComputationState, + e: Entity, + p: Property + ): ProperPropertyComputationResult = { + // Add mapping information (which will be used for computing the final result) + val retrievedProperty = p.asInstanceOf[StringConstancyProperty] + val currentSci = retrievedProperty.stringConstancyInformation + state.fpe2sci.put(state.var2IndexMapping(e.asInstanceOf[P]._1), currentSci) + + // No more dependees => Return the result for this analysis run + val remDependees = dependees.filter(_.e != e) + if (remDependees.isEmpty) { + val interpretationHandler = IntraproceduralInterpretationHandler(state.tac) + val finalSci = new PathTransformer(interpretationHandler).pathToStringTree( + state.computedLeanPath, state.fpe2sci.map { case (k, v) ⇒ (k, ListBuffer(v)) } + ).reduce(true) + Result(data, StringConstancyProperty(finalSci)) + } else { + InterimResult( + data, + StringConstancyProperty.ub, + StringConstancyProperty.lb, + remDependees, + continuation(data, remDependees, state) + ) + } + } + + /** + * Continuation function. + * + * @param data The data that was passed to the `analyze` function. + * @param dependees A list of dependencies that this analysis run depends on. + * @param state The computation state (which was originally captured by `analyze` and possibly + * extended / updated by other methods involved in computing the final result. + * @return This function can either produce a final result or another intermediate result. + */ + private def continuation( + data: P, + dependees: Iterable[EOptionP[Entity, Property]], + state: ComputationState + )(eps: SomeEPS): ProperPropertyComputationResult = eps match { + case FinalP(p) ⇒ processFinalP(data, dependees, state, eps.e, p) + case InterimLUBP(lb, ub) ⇒ InterimResult( + data, lb, ub, dependees, continuation(data, dependees, state) + ) + case _ ⇒ throw new IllegalStateException("Could not process the continuation successfully.") + } + + /** + * Helper / accumulator function for finding dependees. For how dependees are detected, see + * [[findDependentVars]]. Returns a list of pairs of DUVar and the index of the + * [[FlatPathElement.element]] in which it occurs. + */ + private def findDependeesAcc( + subpath: SubPath, + stmts: Array[Stmt[V]], + target: V, + foundDependees: ListBuffer[(V, Int)], + hasTargetBeenSeen: Boolean + ): (ListBuffer[(V, Int)], Boolean) = { + var encounteredTarget = false + subpath match { + case fpe: FlatPathElement ⇒ + if (target.definedBy.contains(fpe.element)) { + encounteredTarget = true + } + // For FlatPathElements, search for DUVars on which the toString method is called + // and where these toString calls are the parameter of an append call + stmts(fpe.element) match { + case ExprStmt(_, outerExpr) ⇒ + if (InterpretationHandler.isStringBuilderBufferAppendCall(outerExpr)) { + val param = outerExpr.asVirtualFunctionCall.params.head.asVar + param.definedBy.filter(_ >= 0).foreach { ds ⇒ + val expr = stmts(ds).asAssignment.expr + if (InterpretationHandler.isStringBuilderBufferToStringCall(expr)) { + foundDependees.append(( + outerExpr.asVirtualFunctionCall.params.head.asVar, + fpe.element + )) + } + } + } + case _ ⇒ + } + (foundDependees, encounteredTarget) + case npe: NestedPathElement ⇒ + npe.element.foreach { nextSubpath ⇒ + if (!encounteredTarget) { + val (_, seen) = findDependeesAcc( + nextSubpath, stmts, target, foundDependees, encounteredTarget + ) + encounteredTarget = seen + } + } + (foundDependees, encounteredTarget) + case _ ⇒ (foundDependees, encounteredTarget) + } + } + + /** + * Takes a `path`, this should be the lean path of a [[Path]], as well as a context in the form + * of statements, `stmts`, and detects all dependees within `path`. Dependees are found by + * looking at all elements in the path, and check whether the argument of an `append` call is a + * value that stems from a `toString` call of a [[StringBuilder]] or [[StringBuffer]]. This + * function then returns the found UVars along with the indices of those append statements. + * + * @note In order to make sure that a [[org.opalj.tac.DUVar]] does not depend on itself, pass + * this variable as `ignore`. + */ + private def findDependentVars( + path: Path, stmts: Array[Stmt[V]], ignore: V + ): mutable.LinkedHashMap[V, Int] = { + val dependees = mutable.LinkedHashMap[V, Int]() + val ignoreNews = InterpretationHandler.findNewOfVar(ignore, stmts) + var wasTargetSeen = false + + path.elements.foreach { nextSubpath ⇒ + if (!wasTargetSeen) { + val (currentDeps, encounteredTarget) = findDependeesAcc( + nextSubpath, stmts, ignore, ListBuffer(), hasTargetBeenSeen = false + ) + wasTargetSeen = encounteredTarget + currentDeps.foreach { nextPair ⇒ + val newExpressions = InterpretationHandler.findNewOfVar(nextPair._1, stmts) + if (ignore != nextPair._1 && ignoreNews != newExpressions) { + dependees.put(nextPair._1, nextPair._2) + } + } + } + } + dependees + } + +} + +sealed trait IntraproceduralStringAnalysisScheduler extends FPCFAnalysisScheduler { + + final def derivedProperty: PropertyBounds = PropertyBounds.lub(StringConstancyProperty) + + final override def uses: Set[PropertyBounds] = Set( + PropertyBounds.ub(TACAI), + PropertyBounds.ub(Callees), + PropertyBounds.lub(StringConstancyProperty) + ) + + final override type InitializationData = IntraproceduralStringAnalysis + final override def init(p: SomeProject, ps: PropertyStore): InitializationData = { + new IntraproceduralStringAnalysis(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 = {} + +} + +/** + * Executor for the lazy analysis. + */ +object LazyIntraproceduralStringAnalysis + extends IntraproceduralStringAnalysisScheduler with FPCFLazyAnalysisScheduler { + + override def register( + p: SomeProject, ps: PropertyStore, analysis: InitializationData + ): FPCFAnalysis = { + val analysis = new IntraproceduralStringAnalysis(p) + ps.registerLazyPropertyComputation(StringConstancyProperty.key, analysis.analyze) + analysis + } + + override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/AbstractStringInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/AbstractStringInterpreter.scala new file mode 100644 index 0000000000..be66ace930 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/AbstractStringInterpreter.scala @@ -0,0 +1,213 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.Property +import org.opalj.fpcf.PropertyStore +import org.opalj.value.ValueInformation +import org.opalj.br.cfg.CFG +import org.opalj.br.Method +import org.opalj.br.analyses.DeclaredMethods +import org.opalj.br.DefinedMethod +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.cg.Callees +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.Assignment +import org.opalj.tac.DUVar +import org.opalj.tac.Expr +import org.opalj.tac.ExprStmt +import org.opalj.tac.FunctionCall +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.InterproceduralInterpretationHandler +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.fpcf.analyses.string_analysis.NonFinalFunctionArgs +import org.opalj.tac.fpcf.analyses.string_analysis.NonFinalFunctionArgsPos +import org.opalj.tac.fpcf.analyses.string_analysis.P + +/** + * @param cfg The control flow graph that underlies the instruction to interpret. + * @param exprHandler In order to interpret an instruction, it might be necessary to interpret + * another instruction in the first place. `exprHandler` makes this possible. + * + * @note The abstract type [[InterpretationHandler]] allows the handling of different styles (e.g., + * intraprocedural and interprocedural). Thus, implementation of this class are required to + * clearly indicate what kind of [[InterpretationHandler]] they expect in order to ensure the + * desired behavior and not confuse developers. + * + * @author Patrick Mell + */ +abstract class AbstractStringInterpreter( + protected val cfg: CFG[Stmt[V], TACStmts[V]], + protected val exprHandler: InterpretationHandler +) { + + type T <: Any + + /** + * Returns the EPS retrieved from querying the given property store for the given method as well + * as the TAC, if it could already be determined. If not, thus function registers a dependee + * within the given state. + * + * @param ps The property store to use. + * @param m The method to get the TAC for. + * @param s The computation state whose dependees might be extended in case the TAC is not + * immediately ready. + * @return Returns (eps, tac). + */ + protected def getTACAI( + ps: PropertyStore, + m: Method, + s: InterproceduralComputationState + ): (EOptionP[Method, TACAI], Option[TACode[TACMethodParameter, V]]) = { + val tacai = ps(m, TACAI.key) + if (tacai.hasUBP) { + (tacai, tacai.ub.tac) + } else { + if (tacai.isRefinable) { + s.dependees = tacai :: s.dependees + } + (tacai, None) + } + } + + /** + * This function returns all methods for a given `pc` among a set of `declaredMethods`. The + * second return value indicates whether at least one method has an unknown body (if `true`, + * then there is such a method). + */ + protected def getMethodsForPC( + implicit + pc: Int, ps: PropertyStore, callees: Callees, declaredMethods: DeclaredMethods + ): (List[Method], Boolean) = { + var hasMethodWithUnknownBody = false + val methods = ListBuffer[Method]() + callees.callees(pc).foreach { + case definedMethod: DefinedMethod ⇒ methods.append(definedMethod.definedMethod) + case _ ⇒ hasMethodWithUnknownBody = true + } + + (methods.sortBy(_.classFile.fqn).toList, hasMethodWithUnknownBody) + } + + /** + * `getParametersForPCs` takes a list of program counters, `pcs`, as well as the TACode on which + * `pcs` is based. This function then extracts the parameters of all function calls from the + * given `pcs` and returns them. + */ + protected def getParametersForPCs( + pcs: Iterable[Int], + tac: TACode[TACMethodParameter, DUVar[ValueInformation]] + ): List[Seq[Expr[V]]] = { + val paramLists = ListBuffer[Seq[Expr[V]]]() + pcs.map(tac.pcToIndex).foreach { stmtIndex ⇒ + val params = tac.stmts(stmtIndex) match { + case ExprStmt(_, vfc: FunctionCall[V]) ⇒ vfc.params + case Assignment(_, _, fc: FunctionCall[V]) ⇒ fc.params + case _ ⇒ Seq() + } + if (params.nonEmpty) { + paramLists.append(params) + } + } + paramLists.toList + } + + /** + * evaluateParameters takes a list of parameters, `params`, as produced, e.g., by + * [[AbstractStringInterpreter.getParametersForPCs]], and an interpretation handler, `iHandler` + * and interprets the given parameters. The result list has the following format: The outer list + * corresponds to the lists of parameters passed to a function / method, the list in the middle + * corresponds to such lists and the inner-most list corresponds to the results / + * interpretations (this list is required as a concrete parameter may have more than one + * definition site). + * For housekeeping, this function takes the function call, `funCall`, of which parameters are + * to be evaluated as well as function argument positions, `functionArgsPos`, and a mapping from + * entities to functions, `entity2function`. + */ + protected def evaluateParameters( + params: List[Seq[Expr[V]]], + iHandler: InterproceduralInterpretationHandler, + funCall: FunctionCall[V], + functionArgsPos: NonFinalFunctionArgsPos, + entity2function: mutable.Map[P, ListBuffer[FunctionCall[V]]] + ): NonFinalFunctionArgs = params.zipWithIndex.map { + case (nextParamList, outerIndex) ⇒ + nextParamList.zipWithIndex.map { + case (nextParam, middleIndex) ⇒ + nextParam.asVar.definedBy.toArray.sorted.zipWithIndex.map { + case (ds, innerIndex) ⇒ + val ep = iHandler.processDefSite(ds) + if (ep.isRefinable) { + if (!functionArgsPos.contains(funCall)) { + functionArgsPos(funCall) = mutable.Map() + } + val e = ep.e.asInstanceOf[P] + functionArgsPos(funCall)(e) = (outerIndex, middleIndex, innerIndex) + if (!entity2function.contains(e)) { + entity2function(e) = ListBuffer() + } + entity2function(e).append(funCall) + } + ep + }.to[ListBuffer] + }.to[ListBuffer] + }.to[ListBuffer] + + /** + * This function checks whether the interpretation of parameters, as, e.g., produced by + * [[evaluateParameters()]], is final or not and returns all refineables as a list. Hence, if + * this function returns an empty list, all parameters are fully evaluated. + */ + protected def getNonFinalParameters( + evaluatedParameters: Seq[Seq[Seq[EOptionP[Entity, StringConstancyProperty]]]] + ): List[EOptionP[Entity, StringConstancyProperty]] = + evaluatedParameters.flatten.flatten.filter { _.isRefinable }.toList + + /** + * convertEvaluatedParameters takes a list of evaluated / interpreted parameters as, e.g., + * produced by [[evaluateParameters]] and transforms these into a list of lists where the inner + * lists are the reduced [[StringConstancyInformation]]. Note that this function assumes that + * all results in the inner-most sequence are final! + */ + protected def convertEvaluatedParameters( + evaluatedParameters: Seq[Seq[Seq[EOptionP[Entity, Property]]]] + ): ListBuffer[ListBuffer[StringConstancyInformation]] = evaluatedParameters.map { paramList ⇒ + paramList.map { param ⇒ + StringConstancyInformation.reduceMultiple( + param.map { + _.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + } + ) + }.to[ListBuffer] + }.to[ListBuffer] + + /** + * + * @param instr The instruction that is to be interpreted. It is the responsibility of + * implementations to make sure that an instruction is properly and comprehensively + * evaluated. + * @param defSite The definition site that corresponds to the given instruction. `defSite` is + * not necessary for processing `instr`, however, may be used, e.g., for + * housekeeping purposes. Thus, concrete implementations should indicate whether + * this value is of importance for (further) processing. + * @return The interpreted instruction. A neutral StringConstancyProperty contained in the + * result indicates that an instruction was not / could not be interpreted (e.g., + * because it is not supported or it was processed before). + *

+ * As demanded by [[InterpretationHandler]], the entity of the result should be the + * definition site. However, as interpreters know the instruction to interpret but not + * the definition site, this function returns the interpreted instruction as entity. + * Thus, the entity needs to be replaced by the calling client. + */ + def interpret(instr: T, defSite: Int): EOptionP[Entity, Property] + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/InterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/InterpretationHandler.scala new file mode 100644 index 0000000000..bad135fd9b --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/InterpretationHandler.scala @@ -0,0 +1,288 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.Property +import org.opalj.value.ValueInformation +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.Assignment +import org.opalj.tac.Expr +import org.opalj.tac.New +import org.opalj.tac.Stmt +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.DUVar +import org.opalj.tac.GetField +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralStringAnalysis + +abstract class InterpretationHandler(tac: TACode[TACMethodParameter, DUVar[ValueInformation]]) { + + protected val stmts: Array[Stmt[DUVar[ValueInformation]]] = tac.stmts + protected val cfg: CFG[Stmt[DUVar[ValueInformation]], TACStmts[DUVar[ValueInformation]]] = + tac.cfg + + /** + * A list of definition sites that have already been processed. Store it as a map for constant + * loop-ups (the value is not relevant and thus set to [[Unit]]). + */ + protected val processedDefSites: mutable.Map[Int, Unit] = mutable.Map() + + /** + * Processes a given definition site. That is, this function determines the interpretation of + * the specified instruction. + * + * @param defSite The definition site to process. Make sure that (1) the value is >= 0, (2) it + * actually exists, and (3) can be processed by one of the subclasses of + * [[AbstractStringInterpreter]] (in case (3) is violated, an + * [[IllegalArgumentException]] will be thrown. + * @param params For a (precise) interpretation, (method / function) parameter values might be + * necessary. They can be leveraged using this value. The implementing classes + * should make sure that (1) they handle the case when no parameters are given + * and (2)they have a proper mapping from the definition sites within used methods + * to the indices in `params` (as the definition sites of parameters are < 0). + * @return Returns the result of the interpretation. Note that depending on the concrete + * interpreter either a final or an intermediate result can be returned! + * In case the rules listed above or the ones of the different concrete interpreters are + * not met, the neutral [[org.opalj.br.fpcf.properties.StringConstancyProperty]] element + * will be encapsulated in the result (see + * [[org.opalj.br.fpcf.properties.StringConstancyProperty.isTheNeutralElement]]). + * The entity of the result will be the given `defSite`. + */ + def processDefSite( + defSite: Int, + params: List[Seq[StringConstancyInformation]] = List() + ): EOptionP[Entity, Property] + + /** + * [[InterpretationHandler]]s keeps an internal state for correct and faster processing. As + * long as a single object within a CFG is analyzed, there is no need to reset the state. + * However, when analyzing a second object (even the same object) it is necessary to call + * `reset` to reset the internal state. Otherwise, incorrect results will be produced. + * (Alternatively, another instance of an implementation of [[InterpretationHandler]] could be + * instantiated.) + */ + def reset(): Unit = { + processedDefSites.clear() + } + +} + +object InterpretationHandler { + + /** + * Checks whether an expression contains a call to [[StringBuilder#toString]] or + * [[StringBuffer#toString]]. + * + * @param expr The expression that is to be checked. + * @return Returns true if `expr` is a call to `toString` of [[StringBuilder]] or + * [[StringBuffer]]. + */ + def isStringBuilderBufferToStringCall(expr: Expr[V]): Boolean = + expr match { + case VirtualFunctionCall(_, clazz, _, name, _, _, _) ⇒ + val className = clazz.mostPreciseObjectType.fqn + (className == "java/lang/StringBuilder" || className == "java/lang/StringBuffer") && + name == "toString" + case _ ⇒ false + } + + /** + * Checks whether the given expression is a string constant / string literal. + * + * @param expr The expression to check. + * @return Returns `true` if the given expression is a string constant / literal and `false` + * otherwise. + */ + def isStringConstExpression(expr: Expr[V]): Boolean = if (expr.isStringConst) { + true + } else { + if (expr.isVar) { + val value = expr.asVar.value + value.isReferenceValue && value.asReferenceValue.upperTypeBound.exists { + _.toJava == "java.lang.String" + } + } else { + false + } + } + + /** + * Returns `true` if the given expressions is a primitive number type + */ + def isPrimitiveNumberTypeExpression(expr: Expr[V]): Boolean = + expr.asVar.value.isPrimitiveValue && + InterproceduralStringAnalysis.isSupportedPrimitiveNumberType( + expr.asVar.value.asPrimitiveValue.primitiveType.toJava + ) + + /** + * Checks whether an expression contains a call to [[StringBuilder#append]] or + * [[StringBuffer#append]]. + * + * @param expr The expression that is to be checked. + * @return Returns true if `expr` is a call to `append` of [[StringBuilder]] or + * [[StringBuffer]]. + */ + def isStringBuilderBufferAppendCall(expr: Expr[V]): Boolean = { + expr match { + case VirtualFunctionCall(_, clazz, _, name, _, _, _) ⇒ + val className = clazz.toJavaClass.getName + (className == "java.lang.StringBuilder" || className == "java.lang.StringBuffer") && + name == "append" + case _ ⇒ false + } + } + + /** + * Helper function for [[findDefSiteOfInit]]. + */ + private def findDefSiteOfInitAcc( + toString: VirtualFunctionCall[V], stmts: Array[Stmt[V]] + ): List[Int] = { + // TODO: Check that we deal with an instance of AbstractStringBuilder + if (toString.name != "toString") { + return List() + } + + val defSites = ListBuffer[Int]() + val stack = mutable.Stack[Int](toString.receiver.asVar.definedBy.filter(_ >= 0).toArray: _*) + val seenElements: mutable.Map[Int, Unit] = mutable.Map() + while (stack.nonEmpty) { + val next = stack.pop() + stmts(next) match { + case a: Assignment[V] ⇒ + a.expr match { + case _: New ⇒ + defSites.append(next) + case vfc: VirtualFunctionCall[V] ⇒ + val recDefSites = vfc.receiver.asVar.definedBy.filter(_ >= 0).toArray + // recDefSites.isEmpty => Definition site is a parameter => Use the + // current function call as a def site + if (recDefSites.nonEmpty) { + stack.pushAll(recDefSites.filter(!seenElements.contains(_))) + } else { + defSites.append(next) + } + case _: GetField[V] ⇒ + defSites.append(next) + case _ ⇒ // E.g., NullExpr + } + case _ ⇒ + } + seenElements(next) = Unit + } + + defSites.sorted.toList + } + + /** + * Determines the definition sites of the initializations of the base object of `duvar`. This + * function assumes that the definition sites refer to `toString` calls. + * + * @param duvar The `DUVar` to get the initializations of the base object for. + * @param stmts The search context for finding the relevant information. + * @return Returns the definition sites of the base object. + */ + def findDefSiteOfInit(duvar: V, stmts: Array[Stmt[V]]): List[Int] = { + val defSites = ListBuffer[Int]() + duvar.definedBy.foreach { ds ⇒ + defSites.appendAll(stmts(ds).asAssignment.expr match { + case vfc: VirtualFunctionCall[V] ⇒ findDefSiteOfInitAcc(vfc, stmts) + // The following case is, e.g., for {NonVirtual, Static}FunctionCalls + case _ ⇒ List(ds) + }) + } + // If no init sites could be determined, use the definition sites of the UVar + if (defSites.isEmpty) { + defSites.appendAll(duvar.definedBy.toArray) + } + + defSites.distinct.sorted.toList + } + + /** + * Determines the [[New]] expressions that belongs to a given `duvar`. + * + * @param duvar The [[org.opalj.tac.DUVar]] to get the [[New]]s for. + * @param stmts The context to search in, e.g., the surrounding method. + * @return Returns all found [[New]] expressions. + */ + def findNewOfVar(duvar: V, stmts: Array[Stmt[V]]): List[New] = { + val news = ListBuffer[New]() + + // HINT: It might be that the search has to be extended to further cases + duvar.definedBy.filter(_ >= 0).foreach { ds ⇒ + stmts(ds) match { + // E.g., a call to `toString` or `append` + case Assignment(_, _, vfc: VirtualFunctionCall[V]) ⇒ + vfc.receiver.asVar.definedBy.filter(_ >= 0).foreach { innerDs ⇒ + stmts(innerDs) match { + case Assignment(_, _, expr: New) ⇒ + news.append(expr) + case Assignment(_, _, expr: VirtualFunctionCall[V]) ⇒ + val exprReceiverVar = expr.receiver.asVar + // The "if" is to avoid endless recursion + if (duvar.definedBy != exprReceiverVar.definedBy) { + news.appendAll(findNewOfVar(exprReceiverVar, stmts)) + } + case _ ⇒ + } + } + case Assignment(_, _, newExpr: New) ⇒ + news.append(newExpr) + case _ ⇒ + } + } + + news.toList + } + + /** + * @return Returns a [[StringConstancyInformation]] element that describes an `int` value. + * That is, the returned element consists of the value [[StringConstancyLevel.DYNAMIC]], + * [[StringConstancyType.APPEND]], and [[StringConstancyInformation.IntValue]]. + */ + def getConstancyInfoForDynamicInt: StringConstancyInformation = + StringConstancyInformation( + StringConstancyLevel.DYNAMIC, + StringConstancyType.APPEND, + StringConstancyInformation.IntValue + ) + + /** + * @return Returns a [[StringConstancyInformation]] element that describes a `float` value. + * That is, the returned element consists of the value [[StringConstancyLevel.DYNAMIC]], + * [[StringConstancyType.APPEND]], and [[StringConstancyInformation.IntValue]]. + */ + def getConstancyInfoForDynamicFloat: StringConstancyInformation = + StringConstancyInformation( + StringConstancyLevel.DYNAMIC, + StringConstancyType.APPEND, + StringConstancyInformation.FloatValue + ) + + /** + * @return Returns a [[StringConstancyProperty]] element that describes the result of a + * `replace` operation. That is, the returned element currently consists of the value + * [[StringConstancyLevel.DYNAMIC]], [[StringConstancyType.REPLACE]], and + * [[StringConstancyInformation.UnknownWordSymbol]]. + */ + def getStringConstancyPropertyForReplace: StringConstancyProperty = + StringConstancyProperty(StringConstancyInformation( + StringConstancyLevel.DYNAMIC, + StringConstancyType.REPLACE, + StringConstancyInformation.UnknownWordSymbol + )) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/BinaryExprInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/BinaryExprInterpreter.scala new file mode 100644 index 0000000000..f6730147b1 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/BinaryExprInterpreter.scala @@ -0,0 +1,59 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.ComputationalTypeFloat +import org.opalj.br.ComputationalTypeInt +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.BinaryExpr +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * The `BinaryExprInterpreter` is responsible for processing [[BinaryExpr]]ions. A list of currently + * supported binary expressions can be found in the documentation of [[interpret]]. + *

+ * For this interpreter, it is of no relevance what concrete implementation of + * [[InterpretationHandler]] is passed. + * + * @see [[AbstractStringInterpreter]] + * @author Patrick Mell + */ +class BinaryExprInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = BinaryExpr[V] + + /** + * Currently, this implementation supports the interpretation of the following binary + * expressions: + *

    + *
  • [[ComputationalTypeInt]] + *
  • [[ComputationalTypeFloat]]
  • + * + * For all other expressions, a result containing [[StringConstancyProperty.getNeutralElement]] + * will be returned. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val sci = instr.cTpe match { + case ComputationalTypeInt ⇒ InterpretationHandler.getConstancyInfoForDynamicInt + case ComputationalTypeFloat ⇒ InterpretationHandler.getConstancyInfoForDynamicFloat + case _ ⇒ StringConstancyInformation.getNeutralElement + } + FinalEP(instr, StringConstancyProperty(sci)) + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/DoubleValueInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/DoubleValueInterpreter.scala new file mode 100644 index 0000000000..67a9e395dc --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/DoubleValueInterpreter.scala @@ -0,0 +1,47 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.DoubleConst +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * The `DoubleValueInterpreter` is responsible for processing [[DoubleConst]]s. + *

    + * For this implementation, the concrete implementation passed for [[exprHandler]] is not relevant. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class DoubleValueInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = DoubleConst + + /** + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty(StringConstancyInformation( + StringConstancyLevel.CONSTANT, + StringConstancyType.APPEND, + instr.value.toString + ))) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/FloatValueInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/FloatValueInterpreter.scala new file mode 100644 index 0000000000..225b1808ea --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/FloatValueInterpreter.scala @@ -0,0 +1,47 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.FloatConst +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * The `FloatValueInterpreter` is responsible for processing [[FloatConst]]s. + *

    + * For this implementation, the concrete implementation passed for [[exprHandler]] is not relevant. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class FloatValueInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = FloatConst + + /** + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty(StringConstancyInformation( + StringConstancyLevel.CONSTANT, + StringConstancyType.APPEND, + instr.value.toString + ))) + +} \ No newline at end of file diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/IntegerValueInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/IntegerValueInterpreter.scala new file mode 100644 index 0000000000..66caf979d6 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/IntegerValueInterpreter.scala @@ -0,0 +1,47 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.IntConst +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * The `IntegerValueInterpreter` is responsible for processing [[IntConst]]s. + *

    + * For this implementation, the concrete implementation passed for [[exprHandler]] is not relevant. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntegerValueInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = IntConst + + /** + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty(StringConstancyInformation( + StringConstancyLevel.CONSTANT, + StringConstancyType.APPEND, + instr.value.toString + ))) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/NewInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/NewInterpreter.scala new file mode 100644 index 0000000000..bdf088636e --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/NewInterpreter.scala @@ -0,0 +1,45 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.New +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * The `NewInterpreter` is responsible for processing [[New]] expressions. + *

    + * For this implementation, the concrete implementation passed for [[exprHandler]] is not relevant. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class NewInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = New + + /** + * [[New]] expressions do not carry any relevant information in this context (as the initial + * values are not set in a [[New]] expressions but, e.g., in + * [[org.opalj.tac.NonVirtualMethodCall]]s). Consequently, this implementation always returns a + * Result containing [[StringConstancyProperty.getNeutralElement]]. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty.getNeutralElement) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/StringConstInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/StringConstInterpreter.scala new file mode 100644 index 0000000000..bb01cfecbe --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/common/StringConstInterpreter.scala @@ -0,0 +1,52 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.Stmt +import org.opalj.tac.StringConst +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * The `StringConstInterpreter` is responsible for processing [[StringConst]]s. + *

    + * For this interpreter, the given interpretation handler does not play any role. Consequently, any + * implementation may be passed. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class StringConstInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = StringConst + + /** + * The interpretation of a [[StringConst]] always results in a list with one + * [[StringConstancyLevel.CONSTANT]] [[StringConstancyInformation]] element holding the + * stringified value. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty(StringConstancyInformation( + StringConstancyLevel.CONSTANT, + StringConstancyType.APPEND, + instr.value + ))) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/ArrayLoadPreparer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/ArrayLoadPreparer.scala new file mode 100644 index 0000000000..2db756ba09 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/ArrayLoadPreparer.scala @@ -0,0 +1,147 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.ArrayLoad +import org.opalj.tac.ArrayStore +import org.opalj.tac.Assignment +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState + +/** + * The `ArrayPreparationInterpreter` is responsible for preparing [[ArrayLoad]] as well as + * [[ArrayStore]] expressions in an interprocedural fashion. + *

    + * Not all (partial) results are guaranteed to be available at once, thus intermediate results + * might be produced. This interpreter will only compute the parts necessary to later on fully + * assemble the final result for the array interpretation. + * For more information, see the [[interpret]] method. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class ArrayLoadPreparer( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterproceduralInterpretationHandler, + state: InterproceduralComputationState, + params: List[Seq[StringConstancyInformation]] +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = ArrayLoad[V] + + /** + * @note This implementation will extend [[state.fpe2sci]] in a way that it adds the string + * constancy information for each definition site where it can compute a final result. All + * definition sites producing a refineable result will have to be handled later on to + * not miss this information. + * + * @note For this implementation, `defSite` plays a role! + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val results = ListBuffer[EOptionP[Entity, StringConstancyProperty]]() + + val defSites = instr.arrayRef.asVar.definedBy.toArray + val allDefSites = ArrayLoadPreparer.getStoreAndLoadDefSites( + instr, state.tac.stmts + ) + + allDefSites.map { ds ⇒ (ds, exprHandler.processDefSite(ds)) }.foreach { + case (ds, ep) ⇒ + if (ep.isFinal) { + val p = ep.asFinal.p.asInstanceOf[StringConstancyProperty] + state.appendToFpe2Sci(ds, p.stringConstancyInformation) + } + results.append(ep) + } + + // Add information of parameters + defSites.filter(_ < 0).foreach { ds ⇒ + val paramPos = Math.abs(ds + 2) + // lb is the fallback value + val sci = StringConstancyInformation.reduceMultiple(params.map(_(paramPos))) + state.appendToFpe2Sci(ds, sci) + } + + // If there is at least one InterimResult, return one. Otherwise, return a final result + // (to either indicate that further computation are necessary or a final result is already + // present) + val interims = results.find(!_.isFinal) + if (interims.isDefined) { + interims.get + } else { + var resultSci = StringConstancyInformation.reduceMultiple(results.map { + _.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + }) + // It might be that there are no results; in such a case, set the string information to + // the lower bound and manually add an entry to the results list + if (resultSci.isTheNeutralElement) { + resultSci = StringConstancyInformation.lb + } + if (results.isEmpty) { + results.append(FinalEP( + (instr.arrayRef.asVar, state.entity._2), StringConstancyProperty(resultSci) + )) + } + + state.appendToFpe2Sci(defSite, resultSci) + results.head + } + } + +} + +object ArrayLoadPreparer { + + type T = ArrayLoad[V] + + /** + * This function retrieves all definition sites of the array stores and array loads that belong + * to the given instruction. + * + * @param instr The [[ArrayLoad]] instruction to get the definition sites for. + * @param stmts The set of statements to use. + * @return Returns all definition sites associated with the array stores and array loads of the + * given instruction. The result list is sorted in ascending order. + */ + def getStoreAndLoadDefSites(instr: T, stmts: Array[Stmt[V]]): List[Int] = { + val allDefSites = ListBuffer[Int]() + val defSites = instr.arrayRef.asVar.definedBy.toArray + + defSites.filter(_ >= 0).sorted.foreach { next ⇒ + val arrDecl = stmts(next) + val sortedArrDeclUses = arrDecl.asAssignment.targetVar.usedBy.toArray.sorted + // For ArrayStores + sortedArrDeclUses.filter { + stmts(_).isInstanceOf[ArrayStore[V]] + } foreach { f: Int ⇒ + allDefSites.appendAll(stmts(f).asArrayStore.value.asVar.definedBy.toArray) + } + // For ArrayLoads + sortedArrDeclUses.filter { + stmts(_) match { + case Assignment(_, _, _: ArrayLoad[V]) ⇒ true + case _ ⇒ false + } + } foreach { f: Int ⇒ + val defs = stmts(f).asAssignment.expr.asArrayLoad.arrayRef.asVar.definedBy + allDefSites.appendAll(defs.toArray) + } + } + + allDefSites.sorted.toList + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralFieldInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralFieldInterpreter.scala new file mode 100644 index 0000000000..17c4b6a9f5 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralFieldInterpreter.scala @@ -0,0 +1,148 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.EPK +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.PropertyStore +import org.opalj.br.analyses.FieldAccessInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralStringAnalysis +import org.opalj.tac.FieldRead +import org.opalj.tac.PutField +import org.opalj.tac.PutStatic +import org.opalj.tac.Stmt + +/** + * The `InterproceduralFieldInterpreter` is responsible for processing instances of [[FieldRead]]s. + * At this moment, this includes instances of [[PutField]] and [[PutStatic]]. For the processing + * procedure, see [[InterproceduralFieldInterpreter#interpret]]. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class InterproceduralFieldInterpreter( + state: InterproceduralComputationState, + exprHandler: InterproceduralInterpretationHandler, + ps: PropertyStore, + fieldAccessInformation: FieldAccessInformation +) extends AbstractStringInterpreter(state.tac.cfg, exprHandler) { + + override type T = FieldRead[V] + + /** + * Currently, fields are approximated using the following approach. If a field of a type not + * supported by the [[InterproceduralStringAnalysis]] is passed, + * [[StringConstancyInformation.lb]] will be produces. Otherwise, all write accesses are + * considered and analyzed. If a field is not initialized within a constructor or the class + * itself, it will be approximated using all write accesses as well as with the lower bound and + * "null" => in these cases fields are [[StringConstancyLevel.DYNAMIC]]. + * + * @note For this implementation, `defSite` plays a role! + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + // TODO: The approximation of fields might be outsourced into a dedicated analysis. Then, + // one could add a finer-grained processing or provide different abstraction levels. This + // String analysis could then use the field analysis. + val defSitEntity: Integer = defSite + // Unknown type => Cannot further approximate + if (!InterproceduralStringAnalysis.isSupportedType(instr.declaredFieldType)) { + return FinalEP(instr, StringConstancyProperty.lb) + } + // Write accesses exceeds the threshold => approximate with lower bound + val writeAccesses = fieldAccessInformation.writeAccesses(instr.declaringClass, instr.name) + if (writeAccesses.length > state.fieldWriteThreshold) { + return FinalEP(instr, StringConstancyProperty.lb) + } + + var hasInit = false + val results = ListBuffer[EOptionP[Entity, StringConstancyProperty]]() + writeAccesses.foreach { + case (m, pcs) ⇒ pcs.foreach { pc ⇒ + if (m.name == "" || m.name == "") { + hasInit = true + } + val (tacEps, tac) = getTACAI(ps, m, state) + val nextResult = if (tacEps.isRefinable) { + EPK(state.entity, StringConstancyProperty.key) + } else { + tac match { + case Some(methodTac) ⇒ + val stmt = methodTac.stmts(methodTac.pcToIndex(pc)) + val entity = (extractUVarFromPut(stmt), m) + val eps = ps(entity, StringConstancyProperty.key) + if (eps.isRefinable) { + state.dependees = eps :: state.dependees + // We need some mapping from an entity to an index in order for + // the processFinalP to find an entry. We cannot use the given + // def site as this would mark the def site as finalized even + // though it might not be. Thus, we use -1 as it is a safe dummy + // value + state.appendToVar2IndexMapping(entity._1, -1) + } + eps + case _ ⇒ + // No TAC available + FinalEP(defSitEntity, StringConstancyProperty.lb) + } + } + results.append(nextResult) + } + } + + if (results.isEmpty) { + // No methods, which write the field, were found => Field could either be null or + // any value + val possibleStrings = "(^null$|"+StringConstancyInformation.UnknownWordSymbol+")" + val sci = StringConstancyInformation( + StringConstancyLevel.DYNAMIC, possibleStrings = possibleStrings + ) + state.appendToFpe2Sci( + defSitEntity, StringConstancyProperty.lb.stringConstancyInformation + ) + FinalEP(defSitEntity, StringConstancyProperty(sci)) + } else { + // If all results are final, determine all possible values for the field. Otherwise, + // return some intermediate result to indicate that the computation is not yet done + if (results.forall(_.isFinal)) { + // 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 (!hasInit) { + results.append(FinalEP( + instr, StringConstancyProperty(StringConstancyInformation.getNullElement) + )) + } + val finalSci = StringConstancyInformation.reduceMultiple(results.map { + _.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + }) + state.appendToFpe2Sci(defSitEntity, finalSci) + FinalEP(defSitEntity, StringConstancyProperty(finalSci)) + } else { + results.find(!_.isFinal).get + } + } + } + + /** + * This function extracts a DUVar from a given statement which is required to be either of type + * [[PutStatic]] or [[PutField]]. + */ + private def extractUVarFromPut(field: Stmt[V]): V = field match { + case PutStatic(_, _, _, _, value) ⇒ value.asVar + case PutField(_, _, _, _, _, value) ⇒ value.asVar + case _ ⇒ throw new IllegalArgumentException(s"Type of $field is currently not supported!") + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralInterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralInterpretationHandler.scala new file mode 100644 index 0000000000..76814a6b66 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralInterpretationHandler.scala @@ -0,0 +1,430 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.Property +import org.opalj.fpcf.PropertyStore +import org.opalj.fpcf.Result +import org.opalj.value.ValueInformation +import org.opalj.br.analyses.DeclaredMethods +import org.opalj.br.analyses.FieldAccessInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.cg.Callees +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.ai.ImmediateVMExceptionsOriginOffset +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.ArrayLoad +import org.opalj.tac.Assignment +import org.opalj.tac.BinaryExpr +import org.opalj.tac.DoubleConst +import org.opalj.tac.ExprStmt +import org.opalj.tac.FloatConst +import org.opalj.tac.GetField +import org.opalj.tac.IntConst +import org.opalj.tac.New +import org.opalj.tac.NonVirtualFunctionCall +import org.opalj.tac.NonVirtualMethodCall +import org.opalj.tac.StaticFunctionCall +import org.opalj.tac.StringConst +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.VirtualMethodCall +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.BinaryExprInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.DoubleValueInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.FloatValueInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.IntegerValueInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer.ArrayLoadFinalizer +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.NewInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.StringConstInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer.NonVirtualMethodCallFinalizer +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer.VirtualFunctionCallFinalizer +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.DUVar +import org.opalj.tac.GetStatic +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer.GetFieldFinalizer +import org.opalj.tac.SimpleValueConst +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer.StaticFunctionCallFinalizer +import org.opalj.tac.FieldRead +import org.opalj.tac.NewArray +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer.NewArrayFinalizer + +/** + * `InterproceduralInterpretationHandler` is responsible for processing expressions that are + * relevant in order to determine which value(s) a string read operation might have. These + * expressions usually come from the definitions sites of the variable of interest. + *

    + * For this interpretation handler used interpreters (concrete instances of + * [[org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter]]) can + * either return a final or intermediate result. + * + * @author Patrick Mell + */ +class InterproceduralInterpretationHandler( + tac: TACode[TACMethodParameter, DUVar[ValueInformation]], + ps: PropertyStore, + declaredMethods: DeclaredMethods, + fieldAccessInformation: FieldAccessInformation, + state: InterproceduralComputationState +) extends InterpretationHandler(tac) { + + /** + * Processed the given definition site in an interprocedural fashion. + *

    + * + * @inheritdoc + */ + override def processDefSite( + defSite: Int, params: List[Seq[StringConstancyInformation]] = List() + ): EOptionP[Entity, StringConstancyProperty] = { + // Without doing the following conversion, the following compile error will occur: "the + // result type of an implicit conversion must be more specific than org.opalj.fpcf.Entity" + val e: Integer = defSite.toInt + // Function parameters are not evaluated when none are present (this always includes the + // implicit parameter for "this" and for exceptions thrown outside the current function) + if (defSite < 0 && + (params.isEmpty || defSite == -1 || defSite <= ImmediateVMExceptionsOriginOffset)) { + state.appendToInterimFpe2Sci(defSite, StringConstancyInformation.lb) + return FinalEP(e, StringConstancyProperty.lb) + } else if (defSite < 0) { + val sci = getParam(params, defSite) + state.appendToInterimFpe2Sci(defSite, sci) + return FinalEP(e, StringConstancyProperty(sci)) + } else if (processedDefSites.contains(defSite)) { + state.appendToInterimFpe2Sci(defSite, StringConstancyInformation.getNeutralElement) + return FinalEP(e, StringConstancyProperty.getNeutralElement) + } + // Note that def sites referring to constant expressions will be deleted further down + processedDefSites(defSite) = Unit + + val callees = state.callees + stmts(defSite) match { + case Assignment(_, _, expr: StringConst) ⇒ processConstExpr(expr, defSite) + case Assignment(_, _, expr: IntConst) ⇒ processConstExpr(expr, defSite) + case Assignment(_, _, expr: FloatConst) ⇒ processConstExpr(expr, defSite) + case Assignment(_, _, expr: DoubleConst) ⇒ processConstExpr(expr, defSite) + case Assignment(_, _, expr: ArrayLoad[V]) ⇒ + processArrayLoad(expr, defSite, params) + case Assignment(_, _, expr: NewArray[V]) ⇒ + processNewArray(expr, defSite, params) + case Assignment(_, _, expr: New) ⇒ processNew(expr, defSite) + case Assignment(_, _, expr: GetStatic) ⇒ processGetField(expr, defSite) + case ExprStmt(_, expr: GetStatic) ⇒ processGetField(expr, defSite) + case Assignment(_, _, expr: VirtualFunctionCall[V]) ⇒ processVFC(expr, defSite, params) + case ExprStmt(_, expr: VirtualFunctionCall[V]) ⇒ processVFC(expr, defSite, params) + case Assignment(_, _, expr: StaticFunctionCall[V]) ⇒ + processStaticFunctionCall(expr, defSite, params) + case ExprStmt(_, expr: StaticFunctionCall[V]) ⇒ + processStaticFunctionCall(expr, defSite, params) + case Assignment(_, _, expr: BinaryExpr[V]) ⇒ processBinaryExpr(expr, defSite) + case Assignment(_, _, expr: NonVirtualFunctionCall[V]) ⇒ + processNonVirtualFunctionCall(expr, defSite) + case Assignment(_, _, expr: GetField[V]) ⇒ processGetField(expr, defSite) + case vmc: VirtualMethodCall[V] ⇒ + processVirtualMethodCall(vmc, defSite, callees) + case nvmc: NonVirtualMethodCall[V] ⇒ processNonVirtualMethodCall(nvmc, defSite) + case _ ⇒ + state.appendToInterimFpe2Sci(defSite, StringConstancyInformation.getNeutralElement) + FinalEP(e, StringConstancyProperty.getNeutralElement) + } + } + + /** + * Helper / utility function for processing [[StringConst]], [[IntConst]], [[FloatConst]], and + * [[DoubleConst]]. + */ + private def processConstExpr( + constExpr: SimpleValueConst, defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val finalEP = constExpr match { + case ic: IntConst ⇒ new IntegerValueInterpreter(cfg, this).interpret(ic, defSite) + case fc: FloatConst ⇒ new FloatValueInterpreter(cfg, this).interpret(fc, defSite) + case dc: DoubleConst ⇒ new DoubleValueInterpreter(cfg, this).interpret(dc, defSite) + case sc ⇒ new StringConstInterpreter(cfg, this).interpret( + sc.asInstanceOf[StringConst], defSite + ) + } + val sci = finalEP.asFinal.p.stringConstancyInformation + state.appendToFpe2Sci(defSite, sci) + state.appendToInterimFpe2Sci(defSite, sci) + processedDefSites.remove(defSite) + finalEP + } + + /** + * Helper / utility function for processing [[ArrayLoad]]s. + */ + private def processArrayLoad( + expr: ArrayLoad[V], defSite: Int, params: List[Seq[StringConstancyInformation]] + ): EOptionP[Entity, StringConstancyProperty] = { + val r = new ArrayLoadPreparer( + cfg, this, state, params + ).interpret(expr, defSite) + val sci = if (r.isFinal) { + r.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + } else { + processedDefSites.remove(defSite) + StringConstancyInformation.lb + } + state.appendToInterimFpe2Sci(defSite, sci) + r + } + + /** + * Helper / utility function for processing [[NewArray]]s. + */ + private def processNewArray( + expr: NewArray[V], defSite: Int, params: List[Seq[StringConstancyInformation]] + ): EOptionP[Entity, StringConstancyProperty] = { + val r = new NewArrayPreparer( + cfg, this, state, params + ).interpret(expr, defSite) + val sci = if (r.isFinal) { + r.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + } else { + processedDefSites.remove(defSite) + StringConstancyInformation.lb + } + state.appendToInterimFpe2Sci(defSite, sci) + r + } + + /** + * Helper / utility function for processing [[New]] expressions. + */ + private def processNew(expr: New, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val finalEP = new NewInterpreter(cfg, this).interpret( + expr, defSite + ) + val sci = finalEP.asFinal.p.stringConstancyInformation + state.appendToFpe2Sci(defSite, sci) + state.appendToInterimFpe2Sci(defSite, sci) + finalEP + } + + /** + * Helper / utility function for interpreting [[VirtualFunctionCall]]s. + */ + private def processVFC( + expr: VirtualFunctionCall[V], + defSite: Int, + params: List[Seq[StringConstancyInformation]] + ): EOptionP[Entity, StringConstancyProperty] = { + val r = new VirtualFunctionCallPreparationInterpreter( + cfg, this, ps, state, declaredMethods, params + ).interpret(expr, defSite) + // Set whether the virtual function call is fully prepared. This is the case if 1) the + // call was not fully prepared before (no final result available) or 2) the preparation is + // now done (methodPrep2defSite makes sure we have the TAC ready for a method required by + // this virtual function call). + val isFinalResult = r.isFinal + if (!isFinalResult && !state.isVFCFullyPrepared.contains(expr)) { + state.isVFCFullyPrepared(expr) = false + } else if (state.isVFCFullyPrepared.contains(expr) && state.methodPrep2defSite.isEmpty) { + state.isVFCFullyPrepared(expr) = true + } + val isPrepDone = !state.isVFCFullyPrepared.contains(expr) || state.isVFCFullyPrepared(expr) + + // In case no final result could be computed, remove this def site from the list of + // processed def sites to make sure that is can be compute again (when all final + // results are available); we use nonFinalFunctionArgs because if it does not + // contain expr, it can be finalized later on without processing the function again. + // A differentiation between "toString" and other calls is made since toString calls are not + // prepared in the same way as other calls are as toString does not take any arguments that + // might need to be prepared (however, toString needs a finalization procedure) + if (expr.name == "toString" && + (state.nonFinalFunctionArgs.contains(expr) || !isFinalResult)) { + processedDefSites.remove(defSite) + } else if (state.nonFinalFunctionArgs.contains(expr) || !isPrepDone) { + processedDefSites.remove(defSite) + } + + doInterimResultHandling(r, defSite) + r + } + + /** + * Helper / utility function for processing [[StaticFunctionCall]]s. + */ + private def processStaticFunctionCall( + expr: StaticFunctionCall[V], defSite: Int, params: List[Seq[StringConstancyInformation]] + ): EOptionP[Entity, StringConstancyProperty] = { + val r = new InterproceduralStaticFunctionCallInterpreter( + cfg, this, ps, state, params, declaredMethods + ).interpret(expr, defSite) + if (!r.isInstanceOf[Result] || state.nonFinalFunctionArgs.contains(expr)) { + processedDefSites.remove(defSite) + } + doInterimResultHandling(r, defSite) + + r + } + + /** + * Helper / utility function for processing [[BinaryExpr]]s. + */ + private def processBinaryExpr( + expr: BinaryExpr[V], defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + // TODO: For binary expressions, use the underlying domain to retrieve the result of such + // expressions + val result = new BinaryExprInterpreter(cfg, this).interpret(expr, defSite) + val sci = result.asFinal.p.stringConstancyInformation + state.appendToInterimFpe2Sci(defSite, sci) + state.appendToFpe2Sci(defSite, sci) + result + } + + /** + * Helper / utility function for processing [[GetField]]s. + */ + private def processGetField( + expr: FieldRead[V], defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val r = new InterproceduralFieldInterpreter( + state, this, ps, fieldAccessInformation + ).interpret(expr, defSite) + if (r.isRefinable) { + processedDefSites.remove(defSite) + } + doInterimResultHandling(r, defSite) + r + } + + /** + * Helper / utility function for processing [[NonVirtualMethodCall]]s. + */ + private def processNonVirtualFunctionCall( + expr: NonVirtualFunctionCall[V], defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val r = new InterproceduralNonVirtualFunctionCallInterpreter( + cfg, this, ps, state, declaredMethods + ).interpret(expr, defSite) + if (r.isRefinable || state.nonFinalFunctionArgs.contains(expr)) { + processedDefSites.remove(defSite) + } + doInterimResultHandling(r, defSite) + r + } + + /** + * Helper / utility function for processing [[VirtualMethodCall]]s. + */ + def processVirtualMethodCall( + expr: VirtualMethodCall[V], defSite: Int, callees: Callees + ): EOptionP[Entity, StringConstancyProperty] = { + val r = new InterproceduralVirtualMethodCallInterpreter( + cfg, this, callees + ).interpret(expr, defSite) + doInterimResultHandling(r, defSite) + r + } + + /** + * Helper / utility function for processing [[NonVirtualMethodCall]]s. + */ + private def processNonVirtualMethodCall( + nvmc: NonVirtualMethodCall[V], defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val r = new InterproceduralNonVirtualMethodCallInterpreter( + cfg, this, ps, state, declaredMethods + ).interpret(nvmc, defSite) + r match { + case FinalEP(_, p: StringConstancyProperty) ⇒ + state.appendToInterimFpe2Sci(defSite, p.stringConstancyInformation) + state.appendToFpe2Sci(defSite, p.stringConstancyInformation) + case _ ⇒ + state.appendToInterimFpe2Sci(defSite, StringConstancyInformation.lb) + processedDefSites.remove(defSite) + } + r + } + + /** + * This function takes a result, which can be final or not, as well as a definition site. This + * function handles the steps necessary to provide information for computing intermediate + * results. + */ + private def doInterimResultHandling( + result: EOptionP[Entity, Property], defSite: Int + ): Unit = { + val sci = if (result.isFinal) { + result.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + } else { + StringConstancyInformation.lb + } + state.appendToInterimFpe2Sci(defSite, sci) + } + + /** + * This function takes parameters and a definition site and extracts the desired parameter from + * the given list of parameters. Note that `defSite` is required to be <= -2. + */ + private def getParam( + params: Seq[Seq[StringConstancyInformation]], defSite: Int + ): StringConstancyInformation = { + val paramPos = Math.abs(defSite + 2) + if (params.exists(_.length <= paramPos)) { + StringConstancyInformation.lb + } else { + val paramScis = params.map(_(paramPos)).distinct + StringConstancyInformation.reduceMultiple(paramScis) + } + } + + /** + * Finalized a given definition state. + */ + def finalizeDefSite( + defSite: Int, state: InterproceduralComputationState + ): Unit = { + if (defSite < 0) { + state.appendToFpe2Sci(defSite, getParam(state.params, defSite), reset = true) + } else { + stmts(defSite) match { + case nvmc: NonVirtualMethodCall[V] ⇒ + NonVirtualMethodCallFinalizer(state).finalizeInterpretation(nvmc, defSite) + case Assignment(_, _, al: ArrayLoad[V]) ⇒ + ArrayLoadFinalizer(state, cfg).finalizeInterpretation(al, defSite) + case Assignment(_, _, na: NewArray[V]) ⇒ + NewArrayFinalizer(state, cfg).finalizeInterpretation(na, defSite) + case Assignment(_, _, vfc: VirtualFunctionCall[V]) ⇒ + VirtualFunctionCallFinalizer(state, cfg).finalizeInterpretation(vfc, defSite) + case ExprStmt(_, vfc: VirtualFunctionCall[V]) ⇒ + VirtualFunctionCallFinalizer(state, cfg).finalizeInterpretation(vfc, defSite) + case Assignment(_, _, fr: FieldRead[V]) ⇒ + GetFieldFinalizer(state).finalizeInterpretation(fr, defSite) + case ExprStmt(_, fr: FieldRead[V]) ⇒ + GetFieldFinalizer(state).finalizeInterpretation(fr, defSite) + case Assignment(_, _, sfc: StaticFunctionCall[V]) ⇒ + StaticFunctionCallFinalizer(state).finalizeInterpretation(sfc, defSite) + case ExprStmt(_, sfc: StaticFunctionCall[V]) ⇒ + StaticFunctionCallFinalizer(state).finalizeInterpretation(sfc, defSite) + case _ ⇒ state.appendToFpe2Sci( + defSite, StringConstancyProperty.lb.stringConstancyInformation, reset = true + ) + } + } + } + +} + +object InterproceduralInterpretationHandler { + + /** + * @see [[org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural.IntraproceduralInterpretationHandler]] + */ + def apply( + tac: TACode[TACMethodParameter, DUVar[ValueInformation]], + ps: PropertyStore, + declaredMethods: DeclaredMethods, + fieldAccessInformation: FieldAccessInformation, + state: InterproceduralComputationState + ): InterproceduralInterpretationHandler = new InterproceduralInterpretationHandler( + tac, ps, declaredMethods, fieldAccessInformation, state + ) + +} \ No newline at end of file diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralNonVirtualFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralNonVirtualFunctionCallInterpreter.scala new file mode 100644 index 0000000000..7ed25ed155 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralNonVirtualFunctionCallInterpreter.scala @@ -0,0 +1,87 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.EPK +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.PropertyStore +import org.opalj.fpcf.Result +import org.opalj.br.analyses.DeclaredMethods +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.NonVirtualFunctionCall +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.ReturnValue +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState + +/** + * The `InterproceduralNonVirtualFunctionCallInterpreter` is responsible for processing + * [[NonVirtualFunctionCall]]s in an interprocedural fashion. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class InterproceduralNonVirtualFunctionCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterproceduralInterpretationHandler, + ps: PropertyStore, + state: InterproceduralComputationState, + declaredMethods: DeclaredMethods +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = NonVirtualFunctionCall[V] + + /** + * Currently, [[NonVirtualFunctionCall]]s are not supported. Thus, this function always returns + * a list with a single element consisting of + * [[org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel.DYNAMIC]], + * [[org.opalj.br.fpcf.properties.string_definition.StringConstancyType.APPEND]] and + * [[org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation.UnknownWordSymbol]]. + * + * @note For this implementation, `defSite` plays a role! + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val methods = getMethodsForPC(instr.pc, ps, state.callees, declaredMethods) + if (methods._1.isEmpty) { + // No methods available => Return lower bound + return FinalEP(instr, StringConstancyProperty.lb) + } + val m = methods._1.head + + val (_, tac) = getTACAI(ps, m, state) + if (tac.isDefined) { + state.removeFromMethodPrep2defSite(m, defSite) + // TAC available => Get return UVars and start the string analysis + val returns = tac.get.stmts.filter(_.isInstanceOf[ReturnValue[V]]) + if (returns.isEmpty) { + // A function without returns, e.g., because it is guaranteed to throw an exception, + // is approximated with the lower bound + FinalEP(instr, StringConstancyProperty.lb) + } else { + val results = returns.map { ret ⇒ + val uvar = ret.asInstanceOf[ReturnValue[V]].expr.asVar + val entity = (uvar, m) + + val eps = ps(entity, StringConstancyProperty.key) + if (eps.isRefinable) { + state.dependees = eps :: state.dependees + state.appendToVar2IndexMapping(uvar, defSite) + } + eps + } + results.find(!_.isInstanceOf[Result]).getOrElse(results.head) + } + } else { + state.appendToMethodPrep2defSite(m, defSite) + EPK(state.entity, StringConstancyProperty.key) + } + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralNonVirtualMethodCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralNonVirtualMethodCallInterpreter.scala new file mode 100644 index 0000000000..5746452b1e --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralNonVirtualMethodCallInterpreter.scala @@ -0,0 +1,105 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.PropertyStore +import org.opalj.br.analyses.DeclaredMethods +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.NonVirtualMethodCall +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState + +/** + * The `InterproceduralNonVirtualMethodCallInterpreter` is responsible for processing + * [[NonVirtualMethodCall]]s in an interprocedural fashion. + * For supported method calls, see the documentation of the `interpret` function. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class InterproceduralNonVirtualMethodCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterproceduralInterpretationHandler, + ps: PropertyStore, + state: InterproceduralComputationState, + declaredMethods: DeclaredMethods +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = NonVirtualMethodCall[V] + + /** + * Currently, this function supports the interpretation of the following non virtual methods: + *

      + *
    • + * `<init>`, when initializing an object (for this case, currently zero constructor or + * one constructor parameter are supported; if more params are available, only the very first + * one is interpreted). + *
    • + *
    + * For all other calls, an empty list will be returned at the moment. + * + * @note For this implementation, `defSite` plays a role! + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret( + instr: NonVirtualMethodCall[V], defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val e: Integer = defSite + instr.name match { + case "" ⇒ interpretInit(instr, e) + case _ ⇒ FinalEP(e, StringConstancyProperty.getNeutralElement) + } + } + + /** + * Processes an `<init>` method call. If it has no parameters, + * [[StringConstancyProperty.getNeutralElement]] will be returned. Otherwise, only the very + * first parameter will be evaluated and its result returned (this is reasonable as both, + * [[StringBuffer]] and [[StringBuilder]], have only constructors with <= 1 arguments and only + * these are currently interpreted). + */ + private def interpretInit( + init: NonVirtualMethodCall[V], defSite: Integer + ): EOptionP[Entity, StringConstancyProperty] = { + init.params.size match { + case 0 ⇒ FinalEP(defSite, StringConstancyProperty.getNeutralElement) + case _ ⇒ + val results = init.params.head.asVar.definedBy.map { ds: Int ⇒ + (ds, exprHandler.processDefSite(ds, List())) + } + if (results.forall(_._2.isFinal)) { + // Final result is available + val reduced = StringConstancyInformation.reduceMultiple(results.map { r ⇒ + val prop = r._2.asFinal.p.asInstanceOf[StringConstancyProperty] + prop.stringConstancyInformation + }) + FinalEP(defSite, StringConstancyProperty(reduced)) + } else { + // Some intermediate results => register necessary information from final + // results and return an intermediate result + val returnIR = results.find(r ⇒ !r._2.isFinal).get._2 + results.foreach { + case (ds, r) ⇒ + if (r.isFinal) { + val p = r.asFinal.p.asInstanceOf[StringConstancyProperty] + state.appendToFpe2Sci( + ds, p.stringConstancyInformation, reset = true + ) + } + case _ ⇒ + } + returnIR + } + } + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralStaticFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralStaticFunctionCallInterpreter.scala new file mode 100644 index 0000000000..56789f61e6 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralStaticFunctionCallInterpreter.scala @@ -0,0 +1,191 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import scala.util.Try + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.EPK +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.PropertyStore +import org.opalj.br.analyses.DeclaredMethods +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.StaticFunctionCall +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralStringAnalysis +import org.opalj.tac.ReturnValue + +/** + * The `InterproceduralStaticFunctionCallInterpreter` is responsible for processing + * [[StaticFunctionCall]]s in an interprocedural fashion. + *

    + * For supported method calls, see the documentation of the `interpret` function. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class InterproceduralStaticFunctionCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterproceduralInterpretationHandler, + ps: PropertyStore, + state: InterproceduralComputationState, + params: List[Seq[StringConstancyInformation]], + declaredMethods: DeclaredMethods +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = StaticFunctionCall[V] + + /** + * This function always returns a list with a single element consisting of + * [[org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel.DYNAMIC]], + * [[org.opalj.br.fpcf.properties.string_definition.StringConstancyType.APPEND]], and + * [[org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation.UnknownWordSymbol]]. + * + * @note For this implementation, `defSite` plays a role! + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + if (instr.declaringClass.fqn == "java/lang/String" && instr.name == "valueOf") { + processStringValueOf(instr) + } else { + processArbitraryCall(instr, defSite) + } + } + + /** + * A function for processing calls to [[String#valueOf]]. This function assumes that the passed + * `call` element is actually such a call. + * This function returns an intermediate results if one or more interpretations could not be + * finished. Otherwise, if all definition sites could be fully processed, this function + * returns an instance of Result which corresponds to the result of the interpretation of + * the parameter passed to the call. + */ + private def processStringValueOf( + call: StaticFunctionCall[V] + ): EOptionP[Entity, StringConstancyProperty] = { + val results = call.params.head.asVar.definedBy.toArray.sorted.map { ds ⇒ + exprHandler.processDefSite(ds, params) + } + val interim = results.find(_.isRefinable) + if (interim.isDefined) { + interim.get + } else { + // For char values, we need to do a conversion (as the returned results are integers) + val scis = results.map { r ⇒ + r.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + } + val finalScis = if (call.descriptor.parameterType(0).toJava == "char") { + scis.map { sci ⇒ + if (Try(sci.possibleStrings.toInt).isSuccess) { + sci.copy(possibleStrings = sci.possibleStrings.toInt.toChar.toString) + } else { + sci + } + } + } else { + scis + } + val finalSci = StringConstancyInformation.reduceMultiple(finalScis) + FinalEP(call, StringConstancyProperty(finalSci)) + } + } + + /** + * This function interprets an arbitrary static function call. + */ + private def processArbitraryCall( + instr: StaticFunctionCall[V], defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val methods, _ = getMethodsForPC( + instr.pc, ps, state.callees, declaredMethods + ) + + // Static methods cannot be overwritten, thus 1) we do not need the second return value of + // getMethodsForPC and 2) interpreting the head is enough + if (methods._1.isEmpty) { + state.appendToFpe2Sci(defSite, StringConstancyProperty.lb.stringConstancyInformation) + return FinalEP(instr, StringConstancyProperty.lb) + } + + val m = methods._1.head + val (_, tac) = getTACAI(ps, m, state) + + val directCallSites = state.callees.directCallSites()(ps, declaredMethods) + val relevantPCs = directCallSites.filter { + case (_, calledMethods) ⇒ + calledMethods.exists(m ⇒ + m.name == instr.name && m.declaringClassType == instr.declaringClass) + }.keys + + // Collect all parameters; either from the state if the interpretation of instr was started + // before (in this case, the assumption is that all parameters are fully interpreted) or + // start a new interpretation + val params = if (state.nonFinalFunctionArgs.contains(instr)) { + state.nonFinalFunctionArgs(instr) + } else { + evaluateParameters( + getParametersForPCs(relevantPCs, state.tac), + exprHandler, + instr, + state.nonFinalFunctionArgsPos, + state.entity2Function + ) + } + // Continue only when all parameter information are available + val nonFinalResults = getNonFinalParameters(params) + if (nonFinalResults.nonEmpty) { + if (tac.isDefined) { + val returns = tac.get.stmts.filter(_.isInstanceOf[ReturnValue[V]]) + returns.foreach { ret ⇒ + val entity = (ret.asInstanceOf[ReturnValue[V]].expr.asVar, m) + val eps = ps(entity, StringConstancyProperty.key) + state.dependees = eps :: state.dependees + state.appendToVar2IndexMapping(entity._1, defSite) + } + } + state.nonFinalFunctionArgs(instr) = params + state.appendToMethodPrep2defSite(m, defSite) + return nonFinalResults.head + } + + state.nonFinalFunctionArgs.remove(instr) + state.nonFinalFunctionArgsPos.remove(instr) + val evaluatedParams = convertEvaluatedParameters(params) + if (tac.isDefined) { + state.removeFromMethodPrep2defSite(m, defSite) + // TAC available => Get return UVar and start the string analysis + val returns = tac.get.stmts.filter(_.isInstanceOf[ReturnValue[V]]) + if (returns.isEmpty) { + // A function without returns, e.g., because it is guaranteed to throw an exception, + // is approximated with the lower bound + FinalEP(instr, StringConstancyProperty.lb) + } else { + val results = returns.map { ret ⇒ + val entity = (ret.asInstanceOf[ReturnValue[V]].expr.asVar, m) + InterproceduralStringAnalysis.registerParams(entity, evaluatedParams) + + val eps = ps(entity, StringConstancyProperty.key) + if (eps.isRefinable) { + state.dependees = eps :: state.dependees + state.appendToVar2IndexMapping(entity._1, defSite) + } + eps + } + results.find(_.isRefinable).getOrElse(results.head) + } + } else { + // No TAC => Register dependee and continue + state.appendToMethodPrep2defSite(m, defSite) + EPK(state.entity, StringConstancyProperty.key) + } + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralVirtualMethodCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralVirtualMethodCallInterpreter.scala new file mode 100644 index 0000000000..29808af2c8 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/InterproceduralVirtualMethodCallInterpreter.scala @@ -0,0 +1,62 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.cg.Callees +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.VirtualMethodCall +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter + +/** + * The `InterproceduralVirtualMethodCallInterpreter` is responsible for processing + * [[VirtualMethodCall]]s in an interprocedural fashion. + * For supported method calls, see the documentation of the `interpret` function. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class InterproceduralVirtualMethodCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterproceduralInterpretationHandler, + callees: Callees +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = VirtualMethodCall[V] + + /** + * Currently, this function supports the interpretation of the following virtual methods: + *

      + *
    • + * `setLength`: `setLength` is a method to reset / clear a [[StringBuilder]] / [[StringBuffer]] + * (at least when called with the argument `0`). For simplicity, this interpreter currently + * assumes that 0 is always passed, i.e., the `setLength` method is currently always regarded as + * a reset mechanism. + *
    • + *
    + * For all other calls, an empty list will be returned. + * + * @note For this implementation, `defSite` plays a role! + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val sci = instr.name match { + case "setLength" ⇒ StringConstancyInformation( + StringConstancyLevel.CONSTANT, StringConstancyType.RESET + ) + case _ ⇒ StringConstancyInformation.getNeutralElement + } + FinalEP(instr, StringConstancyProperty(sci)) + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/NewArrayPreparer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/NewArrayPreparer.scala new file mode 100644 index 0000000000..bfcc1a238a --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/NewArrayPreparer.scala @@ -0,0 +1,103 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.ArrayStore +import org.opalj.tac.NewArray + +/** + * The `NewArrayPreparer` is responsible for preparing [[NewArray]] expressions. + *

    + * Not all (partial) results are guaranteed to be available at once, thus intermediate results + * might be produced. This interpreter will only compute the parts necessary to later on fully + * assemble the final result for the array interpretation. + * For more information, see the [[interpret]] method. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class NewArrayPreparer( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterproceduralInterpretationHandler, + state: InterproceduralComputationState, + params: List[Seq[StringConstancyInformation]] +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = NewArray[V] + + /** + * @note This implementation will extend [[state.fpe2sci]] in a way that it adds the string + * constancy information for each definition site where it can compute a final result. All + * definition sites producing a refineable result will have to be handled later on to + * not miss this information. + * + * @note For this implementation, `defSite` plays a role! + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + // Only support for 1-D arrays + if (instr.counts.length != 1) { + FinalEP(instr, StringConstancyProperty.lb) + } + + // Get all sites that define array values and process them + val arrValuesDefSites = + state.tac.stmts(defSite).asAssignment.targetVar.asVar.usedBy.toArray.toList.sorted + var allResults = arrValuesDefSites.filter { + ds ⇒ ds >= 0 && state.tac.stmts(ds).isInstanceOf[ArrayStore[V]] + }.flatMap { ds ⇒ + // ds holds a site an of array stores; these need to be evaluated for the actual values + state.tac.stmts(ds).asArrayStore.value.asVar.definedBy.toArray.toList.sorted.map { d ⇒ + val r = exprHandler.processDefSite(d, params) + if (r.isFinal) { + state.appendToFpe2Sci(d, r.asFinal.p.stringConstancyInformation) + } + r + } + } + + // Add information of parameters + arrValuesDefSites.filter(_ < 0).foreach { ds ⇒ + val paramPos = Math.abs(ds + 2) + // lb is the fallback value + val sci = StringConstancyInformation.reduceMultiple(params.map(_(paramPos))) + state.appendToFpe2Sci(ds, sci) + val e: Integer = ds + allResults ::= FinalEP(e, StringConstancyProperty(sci)) + } + + val interims = allResults.find(!_.isFinal) + if (interims.isDefined) { + interims.get + } else { + var resultSci = StringConstancyInformation.reduceMultiple(allResults.map { + _.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + }) + // It might be that there are no results; in such a case, set the string information to + // the lower bound and manually add an entry to the results list + if (resultSci.isTheNeutralElement) { + resultSci = StringConstancyInformation.lb + } + if (allResults.isEmpty) { + val toAppend = FinalEP(instr, StringConstancyProperty(resultSci)) + allResults = toAppend :: allResults + } + state.appendToFpe2Sci(defSite, resultSci) + FinalEP(Integer.valueOf(defSite), StringConstancyProperty(resultSci)) + } + } + +} + diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/VirtualFunctionCallPreparationInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/VirtualFunctionCallPreparationInterpreter.scala new file mode 100644 index 0000000000..75f6778094 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/VirtualFunctionCallPreparationInterpreter.scala @@ -0,0 +1,392 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.EPK +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.PropertyStore +import org.opalj.fpcf.Result +import org.opalj.br.cfg.CFG +import org.opalj.br.ComputationalTypeFloat +import org.opalj.br.ComputationalTypeInt +import org.opalj.br.ObjectType +import org.opalj.br.analyses.DeclaredMethods +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler +import org.opalj.tac.ReturnValue +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralStringAnalysis +import org.opalj.tac.fpcf.analyses.string_analysis.P + +/** + * The `InterproceduralVirtualFunctionCallInterpreter` is responsible for processing + * [[VirtualFunctionCall]]s in an interprocedural fashion. + * The list of currently supported function calls can be seen in the documentation of [[interpret]]. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class VirtualFunctionCallPreparationInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: InterproceduralInterpretationHandler, + ps: PropertyStore, + state: InterproceduralComputationState, + declaredMethods: DeclaredMethods, + params: List[Seq[StringConstancyInformation]] +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = VirtualFunctionCall[V] + + /** + * Currently, this implementation supports the interpretation of the following function calls: + *

      + *
    • `append`: Calls to the `append` function of [[StringBuilder]] and [[StringBuffer]].
    • + *
    • + * `toString`: Calls to the `append` function of [[StringBuilder]] and [[StringBuffer]]. As + * a `toString` call does not change the state of such an object, an empty list will be + * returned. + *
    • + *
    • + * `replace`: Calls to the `replace` function of [[StringBuilder]] and [[StringBuffer]]. For + * further information how this operation is processed, see + * [[VirtualFunctionCallPreparationInterpreter.interpretReplaceCall]]. + *
    • + *
    • + * Apart from these supported methods, a list with [[StringConstancyProperty.lb]] + * will be returned in case the passed method returns a [[java.lang.String]]. + *
    • + *
    + * + * If none of the above-described cases match, a final result containing + * [[StringConstancyProperty.getNeutralElement]] is returned. + * + * @note For this implementation, `defSite` plays a role! + * + * @note This function takes care of updating [[state.fpe2sci]] as necessary. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val result = instr.name match { + case "append" ⇒ interpretAppendCall(instr, defSite) + case "toString" ⇒ interpretToStringCall(instr) + case "replace" ⇒ interpretReplaceCall(instr) + case _ ⇒ + instr.descriptor.returnType match { + case obj: ObjectType if obj.fqn == "java/lang/String" ⇒ + interpretArbitraryCall(instr, defSite) + case _ ⇒ + val e: Integer = defSite + FinalEP(e, StringConstancyProperty.lb) + } + } + + if (result.isFinal) { + // If the result is final, it is guaranteed to be of type [P, StringConstancyProperty] + val prop = result.asFinal.p.asInstanceOf[StringConstancyProperty] + state.appendToFpe2Sci(defSite, prop.stringConstancyInformation) + } + result + } + + /** + * This function interprets an arbitrary [[VirtualFunctionCall]]. If this method returns a + * [[Result]] instance, the interpretation of this call is already done. Otherwise, a new + * analysis was triggered whose result is not yet ready. In this case, the result needs to be + * finalized later on. + */ + private def interpretArbitraryCall( + instr: T, defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val (methods, _) = getMethodsForPC( + instr.pc, ps, state.callees, declaredMethods + ) + + if (methods.isEmpty) { + return FinalEP(instr, StringConstancyProperty.lb) + } + + val directCallSites = state.callees.directCallSites()(ps, declaredMethods) + val instrClassName = + instr.receiver.asVar.value.asReferenceValue.asReferenceType.mostPreciseObjectType.toJava + val relevantPCs = directCallSites.filter { + case (_, calledMethods) ⇒ calledMethods.exists { m ⇒ + val mClassName = m.declaringClassType.toJava + m.name == instr.name && mClassName == instrClassName + } + }.keys + + // Collect all parameters; either from the state, if the interpretation of instr was started + // before (in this case, the assumption is that all parameters are fully interpreted) or + // start a new interpretation + val params = if (state.nonFinalFunctionArgs.contains(instr)) { + state.nonFinalFunctionArgs(instr) + } else { + evaluateParameters( + getParametersForPCs(relevantPCs, state.tac), + exprHandler, + instr, + state.nonFinalFunctionArgsPos, + state.entity2Function + ) + } + // Continue only when all parameter information are available + val nonFinalResults = getNonFinalParameters(params) + if (nonFinalResults.nonEmpty) { + state.nonFinalFunctionArgs(instr) = params + return nonFinalResults.head + } + + state.nonFinalFunctionArgs.remove(instr) + state.nonFinalFunctionArgsPos.remove(instr) + val evaluatedParams = convertEvaluatedParameters(params) + val results = methods.map { nextMethod ⇒ + val (_, tac) = getTACAI(ps, nextMethod, state) + if (tac.isDefined) { + state.methodPrep2defSite.remove(nextMethod) + val returns = tac.get.stmts.filter(_.isInstanceOf[ReturnValue[V]]) + if (returns.isEmpty) { + // It might be that a function has no return value, e. g., in case it is + // guaranteed to throw an exception + FinalEP(instr, StringConstancyProperty.lb) + } else { + val results = returns.map { ret ⇒ + val entity = (ret.asInstanceOf[ReturnValue[V]].expr.asVar, nextMethod) + InterproceduralStringAnalysis.registerParams(entity, evaluatedParams) + val eps = ps(entity, StringConstancyProperty.key) + eps match { + case r: FinalEP[P, StringConstancyProperty] ⇒ + state.appendToFpe2Sci(defSite, r.p.stringConstancyInformation) + r + case _ ⇒ + state.dependees = eps :: state.dependees + state.appendToVar2IndexMapping(entity._1, defSite) + eps + } + } + results.find(_.isRefinable).getOrElse(results.head) + } + } else { + state.appendToMethodPrep2defSite(nextMethod, defSite) + EPK(state.entity, StringConstancyProperty.key) + } + } + + val finalResults = results.filter(_.isInstanceOf[Result]) + val intermediateResults = results.filter(!_.isInstanceOf[Result]) + if (results.length == finalResults.length) { + finalResults.head + } else { + intermediateResults.head + } + } + + /** + * Function for processing calls to [[StringBuilder#append]] or [[StringBuffer#append]]. Note + * that this function assumes that the given `appendCall` is such a function call! Otherwise, + * the expected behavior cannot be guaranteed. + */ + private def interpretAppendCall( + appendCall: VirtualFunctionCall[V], defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val receiverResults = receiverValuesOfAppendCall(appendCall, state) + val appendResult = valueOfAppendCall(appendCall, state) + + // If there is an intermediate result, return this one (then the final result cannot yet be + // computed) + if (receiverResults.head.isRefinable) { + return receiverResults.head + } else if (appendResult.isRefinable) { + return appendResult + } + + val receiverScis = receiverResults.map { r ⇒ + val p = r.asFinal.p.asInstanceOf[StringConstancyProperty] + p.stringConstancyInformation + } + val appendSci = + appendResult.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + + // The case can occur that receiver and append value are empty; although, it is + // counter-intuitive, this case may occur if both, the receiver and the parameter, have been + // processed before + val areAllReceiversNeutral = receiverScis.forall(_.isTheNeutralElement) + val finalSci = if (areAllReceiversNeutral && appendSci.isTheNeutralElement) { + StringConstancyInformation.getNeutralElement + } // It might be that we have to go back as much as to a New expression. As they do not + // produce a result (= empty list), the if part + else if (areAllReceiversNeutral) { + appendSci + } // The append value might be empty, if the site has already been processed (then this + // information will come from another StringConstancyInformation object + else if (appendSci.isTheNeutralElement) { + StringConstancyInformation.reduceMultiple(receiverScis) + } // Receiver and parameter information are available => Combine them + else { + val receiverSci = StringConstancyInformation.reduceMultiple(receiverScis) + StringConstancyInformation( + StringConstancyLevel.determineForConcat( + receiverSci.constancyLevel, appendSci.constancyLevel + ), + StringConstancyType.APPEND, + receiverSci.possibleStrings + appendSci.possibleStrings + ) + } + + val e: Integer = defSite + FinalEP(e, StringConstancyProperty(finalSci)) + } + + /** + * This function determines the current value of the receiver object of an `append` call. For + * the result list, there is the following convention: A list with one element of type + * [[org.opalj.fpcf.InterimResult]] indicates that a final result for the receiver value could + * not be computed. Otherwise, the result list will contain >= 1 elements of type [[Result]] + * indicating that all final results for the receiver value are available. + * + * @note All final results computed by this function are put int [[state.fpe2sci]] even if the + * returned list contains an [[org.opalj.fpcf.InterimResult]]. + */ + private def receiverValuesOfAppendCall( + call: VirtualFunctionCall[V], state: InterproceduralComputationState + ): List[EOptionP[Entity, StringConstancyProperty]] = { + val defSites = call.receiver.asVar.definedBy.toArray.sorted + + val allResults = defSites.map(ds ⇒ (ds, exprHandler.processDefSite(ds, params))) + val finalResults = allResults.filter(_._2.isFinal) + val finalResultsWithoutNeutralElements = finalResults.filter { + case (_, FinalEP(_, p: StringConstancyProperty)) ⇒ + !p.stringConstancyInformation.isTheNeutralElement + case _ ⇒ false + } + val intermediateResults = allResults.filter(_._2.isRefinable) + + // Extend the state by the final results not being the neutral elements (they might need to + // be finalized later) + finalResultsWithoutNeutralElements.foreach { next ⇒ + val p = next._2.asFinal.p.asInstanceOf[StringConstancyProperty] + val sci = p.stringConstancyInformation + state.appendToFpe2Sci(next._1, sci) + } + + if (intermediateResults.isEmpty) { + finalResults.map(_._2).toList + } else { + List(intermediateResults.head._2) + } + } + + /** + * Determines the (string) value that was passed to a `String{Builder, Buffer}#append` method. + * This function can process string constants as well as function calls as argument to append. + */ + private def valueOfAppendCall( + call: VirtualFunctionCall[V], state: InterproceduralComputationState + ): EOptionP[Entity, StringConstancyProperty] = { + // .head because we want to evaluate only the first argument of append + val param = call.params.head.asVar + val defSites = param.definedBy.toArray.sorted + val values = defSites.map(exprHandler.processDefSite(_, params)) + + // Defer the computation if there is at least one intermediate result + if (values.exists(_.isRefinable)) { + return values.find(_.isRefinable).get + } + + val sciValues = values.map { + _.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + } + val defSitesValueSci = StringConstancyInformation.reduceMultiple(sciValues) + // If defSiteHead points to a "New", value will be the empty list. In that case, process + // the first use site + var newValueSci = StringConstancyInformation.getNeutralElement + if (defSitesValueSci.isTheNeutralElement) { + val headSite = defSites.head + if (headSite < 0) { + newValueSci = StringConstancyInformation.lb + } else { + val ds = cfg.code.instructions(headSite).asAssignment.targetVar.usedBy.toArray.min + val r = exprHandler.processDefSite(ds, params) + // Again, defer the computation if there is no final result (yet) + if (r.isRefinable) { + return r + } else { + val p = r.asFinal.p.asInstanceOf[StringConstancyProperty] + newValueSci = p.stringConstancyInformation + } + } + } else { + newValueSci = defSitesValueSci + } + + val finalSci = param.value.computationalType match { + // For some types, we know the (dynamic) values + case ComputationalTypeInt ⇒ + // The value was already computed above; however, we need to check whether the + // append takes an int value or a char (if it is a constant char, convert it) + if (call.descriptor.parameterType(0).isCharType && + defSitesValueSci.constancyLevel == StringConstancyLevel.CONSTANT) { + if (defSitesValueSci.isTheNeutralElement) { + StringConstancyProperty.lb.stringConstancyInformation + } else { + val charSciValues = sciValues.filter(_.possibleStrings != "") map { sci ⇒ + if (isIntegerValue(sci.possibleStrings)) { + sci.copy(possibleStrings = sci.possibleStrings.toInt.toChar.toString) + } else { + sci + } + } + StringConstancyInformation.reduceMultiple(charSciValues) + } + } else { + newValueSci + } + case ComputationalTypeFloat ⇒ + InterpretationHandler.getConstancyInfoForDynamicFloat + // Otherwise, try to compute + case _ ⇒ + newValueSci + } + + val e: Integer = defSites.head + state.appendToFpe2Sci(e, newValueSci, reset = true) + FinalEP(e, StringConstancyProperty(finalSci)) + } + + /** + * Function for processing calls to [[StringBuilder#toString]] or [[StringBuffer#toString]]. + * Note that this function assumes that the given `toString` is such a function call! Otherwise, + * the expected behavior cannot be guaranteed. + */ + private def interpretToStringCall( + call: VirtualFunctionCall[V] + ): EOptionP[Entity, StringConstancyProperty] = + // TODO: Can it produce an intermediate result??? + exprHandler.processDefSite(call.receiver.asVar.definedBy.head, params) + + /** + * Function for processing calls to [[StringBuilder#replace]] or [[StringBuffer#replace]]. + * (Currently, this function simply approximates `replace` functions by returning the lower + * bound of [[StringConstancyProperty]]). + */ + private def interpretReplaceCall( + instr: VirtualFunctionCall[V] + ): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, InterpretationHandler.getStringConstancyPropertyForReplace) + + /** + * Checks whether a given string is an integer value, i.e. contains only numbers. + */ + private def isIntegerValue(toTest: String): Boolean = toTest.forall(_.isDigit) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/AbstractFinalizer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/AbstractFinalizer.scala new file mode 100644 index 0000000000..bb4bcee0bd --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/AbstractFinalizer.scala @@ -0,0 +1,35 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer + +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState + +/** + * When processing instruction interprocedurally, it is not always possible to compute a final + * result for an instruction. For example, consider the `append` operation of a StringBuilder where + * the `append` argument is a call to another function. This function result is likely to be not + * ready right away, which is why a final result for that `append` operation cannot yet be computed. + *

    + * Implementations of this class finalize the result for instructions. For instance, for `append`, + * a finalizer would use all partial results (receiver and `append` value) to compute the final + * result. However, '''this assumes that all partial results are available when finalizing a + * result!''' + * + * @param state The computation state to use to retrieve partial results and to write the final + * result back. + */ +abstract class AbstractFinalizer(state: InterproceduralComputationState) { + + protected type T <: Any + + /** + * Implementations of this class finalize an instruction of type [[T]] which they are supposed + * to override / refine. This function does not return any result, however, the final result + * computed in this function is to be set in [[state.fpe2sci]] at position `defSite` by concrete + * implementations. + * + * @param instr The instruction that is to be finalized. + * @param defSite The definition site that corresponds to the given instruction. + */ + def finalizeInterpretation(instr: T, defSite: Int): Unit + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/ArrayLoadFinalizer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/ArrayLoadFinalizer.scala new file mode 100644 index 0000000000..71b3ae04f9 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/ArrayLoadFinalizer.scala @@ -0,0 +1,55 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer + +import scala.collection.mutable.ListBuffer + +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.ArrayLoad +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.ArrayLoadPreparer +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState + +/** + * @author Patrick Mell + */ +class ArrayLoadFinalizer( + state: InterproceduralComputationState, cfg: CFG[Stmt[V], TACStmts[V]] +) extends AbstractFinalizer(state) { + + override type T = ArrayLoad[V] + + /** + * Finalizes [[ArrayLoad]]s. + *

    + * @inheritdoc + */ + override def finalizeInterpretation(instr: T, defSite: Int): Unit = { + val allDefSites = ArrayLoadPreparer.getStoreAndLoadDefSites( + instr, state.tac.stmts + ) + + allDefSites.foreach { ds ⇒ + if (!state.fpe2sci.contains(ds)) { + state.iHandler.finalizeDefSite(ds, state) + } + } + + state.fpe2sci(defSite) = ListBuffer(StringConstancyInformation.reduceMultiple( + allDefSites.filter(state.fpe2sci.contains).sorted.flatMap { ds ⇒ + state.fpe2sci(ds) + } + )) + } + +} + +object ArrayLoadFinalizer { + + def apply( + state: InterproceduralComputationState, cfg: CFG[Stmt[V], TACStmts[V]] + ): ArrayLoadFinalizer = new ArrayLoadFinalizer(state, cfg) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/GetFieldFinalizer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/GetFieldFinalizer.scala new file mode 100644 index 0000000000..0f6ce3a2b1 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/GetFieldFinalizer.scala @@ -0,0 +1,33 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer + +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.FieldRead + +class GetFieldFinalizer( + state: InterproceduralComputationState +) extends AbstractFinalizer(state) { + + override protected type T = FieldRead[V] + + /** + * Finalizes [[FieldRead]]s. + *

    + * @inheritdoc + */ + override def finalizeInterpretation(instr: T, defSite: Int): Unit = + // Processing the definition site again is enough as the finalization procedure is only + // called after all dependencies are resolved. Thus, processing the given def site with + // produce a result + state.iHandler.processDefSite(defSite) + +} + +object GetFieldFinalizer { + + def apply( + state: InterproceduralComputationState + ): GetFieldFinalizer = new GetFieldFinalizer(state) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/NewArrayFinalizer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/NewArrayFinalizer.scala new file mode 100644 index 0000000000..2e413c27e8 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/NewArrayFinalizer.scala @@ -0,0 +1,37 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer + +import org.opalj.br.cfg.CFG +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.NewArray + +/** + * @author Patrick Mell + */ +class NewArrayFinalizer( + state: InterproceduralComputationState, cfg: CFG[Stmt[V], TACStmts[V]] +) extends AbstractFinalizer(state) { + + override type T = NewArray[V] + + /** + * Finalizes [[NewArray]]s. + *

    + * @inheritdoc + */ + override def finalizeInterpretation(instr: T, defSite: Int): Unit = + // Simply re-trigger the computation + state.iHandler.processDefSite(defSite) + +} + +object NewArrayFinalizer { + + def apply( + state: InterproceduralComputationState, cfg: CFG[Stmt[V], TACStmts[V]] + ): NewArrayFinalizer = new NewArrayFinalizer(state, cfg) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/NonVirtualMethodCallFinalizer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/NonVirtualMethodCallFinalizer.scala new file mode 100644 index 0000000000..d30b6e53c2 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/NonVirtualMethodCallFinalizer.scala @@ -0,0 +1,47 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer + +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.NonVirtualMethodCall +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.fpcf.analyses.string_analysis.V + +/** + * @author Patrick Mell + */ +class NonVirtualMethodCallFinalizer( + state: InterproceduralComputationState +) extends AbstractFinalizer(state) { + + override type T = NonVirtualMethodCall[V] + + /** + * Finalizes [[NonVirtualMethodCall]]s. + *

    + * @inheritdoc + */ + override def finalizeInterpretation(instr: T, defSite: Int): Unit = { + val toAppend = if (instr.params.nonEmpty) { + instr.params.head.asVar.definedBy.toArray.foreach { ds ⇒ + if (!state.fpe2sci.contains(ds)) { + state.iHandler.finalizeDefSite(ds, state) + } + } + val scis = instr.params.head.asVar.definedBy.toArray.sorted.map { state.fpe2sci } + StringConstancyInformation.reduceMultiple(scis.flatten.toList) + } else { + StringConstancyProperty.lb.stringConstancyInformation + } + state.appendToFpe2Sci(defSite, toAppend, reset = true) + } + +} + +object NonVirtualMethodCallFinalizer { + + def apply( + state: InterproceduralComputationState + ): NonVirtualMethodCallFinalizer = new NonVirtualMethodCallFinalizer(state) + +} \ No newline at end of file diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/StaticFunctionCallFinalizer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/StaticFunctionCallFinalizer.scala new file mode 100644 index 0000000000..8afee79f3a --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/StaticFunctionCallFinalizer.scala @@ -0,0 +1,53 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer + +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.StaticFunctionCall + +/** + * @author Patrick Mell + */ +class StaticFunctionCallFinalizer( + state: InterproceduralComputationState +) extends AbstractFinalizer(state) { + + override type T = StaticFunctionCall[V] + + /** + * Finalizes [[StaticFunctionCall]]s. + *

    + * @inheritdoc + */ + override def finalizeInterpretation(instr: T, defSite: Int): Unit = { + val isValueOf = instr.declaringClass.fqn == "java/lang/String" && instr.name == "valueOf" + val toAppend = if (isValueOf) { + // For the finalization we do not need to consider between chars and non-chars as chars + // are only considered when they are char constants and thus a final result is already + // computed by InterproceduralStaticFunctionCallInterpreter (which is why this method + // will not be called for char parameters) + val defSites = instr.params.head.asVar.definedBy.toArray.sorted + defSites.foreach { ds ⇒ + if (!state.fpe2sci.contains(ds)) { + state.iHandler.finalizeDefSite(ds, state) + } + } + val scis = defSites.map { state.fpe2sci } + StringConstancyInformation.reduceMultiple(scis.flatten.toList) + } else { + StringConstancyProperty.lb.stringConstancyInformation + } + state.appendToFpe2Sci(defSite, toAppend, reset = true) + } + +} + +object StaticFunctionCallFinalizer { + + def apply( + state: InterproceduralComputationState + ): StaticFunctionCallFinalizer = new StaticFunctionCallFinalizer(state) + +} \ No newline at end of file diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/VirtualFunctionCallFinalizer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/VirtualFunctionCallFinalizer.scala new file mode 100644 index 0000000000..c41153dd3a --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/interprocedural/finalizer/VirtualFunctionCallFinalizer.scala @@ -0,0 +1,120 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.finalizer + +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.fpcf.analyses.string_analysis.InterproceduralComputationState + +/** + * @author Patrick Mell + */ +class VirtualFunctionCallFinalizer( + state: InterproceduralComputationState, cfg: CFG[Stmt[V], TACStmts[V]] +) extends AbstractFinalizer(state) { + + override type T = VirtualFunctionCall[V] + + /** + * Finalizes [[VirtualFunctionCall]]s. Currently, this finalizer supports only the "append" and + * "toString" function. + *

    + * @inheritdoc + */ + override def finalizeInterpretation(instr: T, defSite: Int): Unit = { + instr.name match { + case "append" ⇒ finalizeAppend(instr, defSite) + case "toString" ⇒ finalizeToString(instr, defSite) + case _ ⇒ state.appendToFpe2Sci( + defSite, StringConstancyProperty.lb.stringConstancyInformation, reset = true + ) + } + } + + /** + * This function actually finalizes append calls by mimicking the behavior of the corresponding + * interpretation function of + * [[org.opalj.tac.fpcf.analyses.string_analysis.interpretation.interprocedural.VirtualFunctionCallPreparationInterpreter]]. + */ + private def finalizeAppend(instr: T, defSite: Int): Unit = { + val receiverDefSites = instr.receiver.asVar.definedBy.toArray.sorted + receiverDefSites.foreach { ds ⇒ + if (!state.fpe2sci.contains(ds)) { + state.iHandler.finalizeDefSite(ds, state) + } + } + val receiverSci = StringConstancyInformation.reduceMultiple( + receiverDefSites.flatMap { s ⇒ + // As the receiver value is used already here, we do not want it to be used a + // second time (during the final traversing of the path); thus, reset it to have it + // only once in the result, i.e., final tree + val sci = state.fpe2sci(s) + state.appendToFpe2Sci(s, StringConstancyInformation.getNeutralElement, reset = true) + sci + } + ) + + val paramDefSites = instr.params.head.asVar.definedBy.toArray.sorted + paramDefSites.foreach { ds ⇒ + if (!state.fpe2sci.contains(ds)) { + state.iHandler.finalizeDefSite(ds, state) + } + } + val appendSci = if (paramDefSites.forall(state.fpe2sci.contains)) { + StringConstancyInformation.reduceMultiple( + paramDefSites.flatMap(state.fpe2sci(_)) + ) + } else StringConstancyInformation.lb + + val finalSci = if (receiverSci.isTheNeutralElement && appendSci.isTheNeutralElement) { + receiverSci + } else if (receiverSci.isTheNeutralElement) { + appendSci + } else if (appendSci.isTheNeutralElement) { + receiverSci + } else { + StringConstancyInformation( + StringConstancyLevel.determineForConcat( + receiverSci.constancyLevel, appendSci.constancyLevel + ), + StringConstancyType.APPEND, + receiverSci.possibleStrings + appendSci.possibleStrings + ) + } + + state.appendToFpe2Sci(defSite, finalSci, reset = true) + } + + private def finalizeToString(instr: T, defSite: Int): Unit = { + val dependeeSites = instr.receiver.asVar.definedBy + dependeeSites.foreach { nextDependeeSite ⇒ + if (!state.fpe2sci.contains(nextDependeeSite)) { + state.iHandler.finalizeDefSite(nextDependeeSite, state) + } + } + val finalSci = StringConstancyInformation.reduceMultiple( + dependeeSites.toArray.flatMap { ds ⇒ state.fpe2sci(ds) } + ) + // Remove the dependees, such as calls to "toString"; the reason being is that we do not + // duplications (arising from an "append" and a "toString" call) + dependeeSites.foreach { + state.appendToFpe2Sci(_, StringConstancyInformation.getNeutralElement, reset = true) + } + state.appendToFpe2Sci(defSite, finalSci) + } + +} + +object VirtualFunctionCallFinalizer { + + def apply( + state: InterproceduralComputationState, cfg: CFG[Stmt[V], TACStmts[V]] + ): VirtualFunctionCallFinalizer = new VirtualFunctionCallFinalizer(state, cfg) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralArrayInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralArrayInterpreter.scala new file mode 100644 index 0000000000..0724fa5910 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralArrayInterpreter.scala @@ -0,0 +1,83 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.ArrayLoad +import org.opalj.tac.ArrayStore +import org.opalj.tac.Assignment +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter + +/** + * The `IntraproceduralArrayInterpreter` is responsible for processing [[ArrayLoad]] as well as + * [[ArrayStore]] expressions in an intraprocedural fashion. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntraproceduralArrayInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: IntraproceduralInterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = ArrayLoad[V] + + /** + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val stmts = cfg.code.instructions + val children = ListBuffer[StringConstancyInformation]() + // Loop over all possible array values + val defSites = instr.arrayRef.asVar.definedBy.toArray + defSites.filter(_ >= 0).sorted.foreach { next ⇒ + val arrDecl = stmts(next) + val sortedArrDeclUses = arrDecl.asAssignment.targetVar.usedBy.toArray.sorted + // Process ArrayStores + sortedArrDeclUses.filter { + stmts(_).isInstanceOf[ArrayStore[V]] + } foreach { f: Int ⇒ + val sortedDefs = stmts(f).asArrayStore.value.asVar.definedBy.toArray.sorted + children.appendAll(sortedDefs.map { exprHandler.processDefSite(_) }.map { n ⇒ + n.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + }) + } + // Process ArrayLoads + sortedArrDeclUses.filter { + stmts(_) match { + case Assignment(_, _, _: ArrayLoad[V]) ⇒ true + case _ ⇒ false + } + } foreach { f: Int ⇒ + val defs = stmts(f).asAssignment.expr.asArrayLoad.arrayRef.asVar.definedBy + children.appendAll(defs.toArray.sorted.map(exprHandler.processDefSite(_)).map { n ⇒ + n.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + }) + } + } + + // In case it refers to a method parameter, add a dynamic string property + if (defSites.exists(_ < 0)) { + children.append(StringConstancyProperty.lb.stringConstancyInformation) + } + + FinalEP(instr, StringConstancyProperty( + StringConstancyInformation.reduceMultiple( + children.filter(!_.isTheNeutralElement) + ) + )) + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralFieldInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralFieldInterpreter.scala new file mode 100644 index 0000000000..6556cbceb1 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralFieldInterpreter.scala @@ -0,0 +1,43 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.GetField +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter + +/** + * The `IntraproceduralFieldInterpreter` is responsible for processing [[GetField]]s. In this + * implementation, there is currently only primitive support for fields, i.e., they are not analyzed + * but a constant [[org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation]] + * is returned (see [[interpret]] of this class). + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntraproceduralFieldInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: IntraproceduralInterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = GetField[V] + + /** + * Fields are not suppoerted by this implementation. Thus, this function always returns a result + * containing [[StringConstancyProperty.lb]]. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty.lb) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralGetStaticInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralGetStaticInterpreter.scala new file mode 100644 index 0000000000..f79cee0099 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralGetStaticInterpreter.scala @@ -0,0 +1,43 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.GetStatic +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter + +/** + * The `IntraproceduralGetStaticInterpreter` is responsible for processing + * [[org.opalj.tac.GetStatic]]s in an intraprocedural fashion. Thus, they are not analyzed but a + * fixed [[org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation]] is returned + * (see [[interpret]]). + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntraproceduralGetStaticInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: IntraproceduralInterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = GetStatic + + /** + * Currently, this type is not interpreted. Thus, this function always returns a result + * containing [[StringConstancyProperty.lb]]. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty.lb) + +} \ No newline at end of file diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralInterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralInterpretationHandler.scala new file mode 100644 index 0000000000..d4b87324cb --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralInterpretationHandler.scala @@ -0,0 +1,127 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.Property +import org.opalj.value.ValueInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.tac.ArrayLoad +import org.opalj.tac.Assignment +import org.opalj.tac.BinaryExpr +import org.opalj.tac.ExprStmt +import org.opalj.tac.GetField +import org.opalj.tac.IntConst +import org.opalj.tac.New +import org.opalj.tac.NonVirtualFunctionCall +import org.opalj.tac.NonVirtualMethodCall +import org.opalj.tac.StaticFunctionCall +import org.opalj.tac.StringConst +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.VirtualMethodCall +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.DoubleConst +import org.opalj.tac.FloatConst +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.BinaryExprInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.DoubleValueInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.FloatValueInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.IntegerValueInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.NewInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.common.StringConstInterpreter +import org.opalj.tac.DUVar +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode + +/** + * `IntraproceduralInterpretationHandler` is responsible for processing expressions that are + * relevant in order to determine which value(s) a string read operation might have. These + * expressions usually come from the definitions sites of the variable of interest. + *

    + * For this interpretation handler it is crucial that all used interpreters (concrete instances of + * [[org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter]]) return + * a final computation result! + * + * @author Patrick Mell + */ +class IntraproceduralInterpretationHandler( + tac: TACode[TACMethodParameter, DUVar[ValueInformation]] +) extends InterpretationHandler(tac) { + + /** + * Processed the given definition site in an intraprocedural fashion. + *

    + * @inheritdoc + */ + override def processDefSite( + defSite: Int, params: List[Seq[StringConstancyInformation]] = List() + ): EOptionP[Entity, Property] = { + // Without doing the following conversion, the following compile error will occur: "the + // result type of an implicit conversion must be more specific than org.opalj.fpcf.Entity" + val e: Integer = defSite.toInt + // Function parameters are not evaluated but regarded as unknown + if (defSite < 0) { + return FinalEP(e, StringConstancyProperty.lb) + } else if (processedDefSites.contains(defSite)) { + return FinalEP(e, StringConstancyProperty.getNeutralElement) + } + processedDefSites(defSite) = Unit + + val result: EOptionP[Entity, Property] = stmts(defSite) match { + case Assignment(_, _, expr: StringConst) ⇒ + new StringConstInterpreter(cfg, this).interpret(expr, defSite) + case Assignment(_, _, expr: IntConst) ⇒ + new IntegerValueInterpreter(cfg, this).interpret(expr, defSite) + case Assignment(_, _, expr: FloatConst) ⇒ + new FloatValueInterpreter(cfg, this).interpret(expr, defSite) + case Assignment(_, _, expr: DoubleConst) ⇒ + new DoubleValueInterpreter(cfg, this).interpret(expr, defSite) + case Assignment(_, _, expr: ArrayLoad[V]) ⇒ + new IntraproceduralArrayInterpreter(cfg, this).interpret(expr, defSite) + case Assignment(_, _, expr: New) ⇒ + new NewInterpreter(cfg, this).interpret(expr, defSite) + case Assignment(_, _, expr: VirtualFunctionCall[V]) ⇒ + new IntraproceduralVirtualFunctionCallInterpreter( + cfg, this + ).interpret(expr, defSite) + case Assignment(_, _, expr: StaticFunctionCall[V]) ⇒ + new IntraproceduralStaticFunctionCallInterpreter(cfg, this).interpret(expr, defSite) + case Assignment(_, _, expr: BinaryExpr[V]) ⇒ + new BinaryExprInterpreter(cfg, this).interpret(expr, defSite) + case Assignment(_, _, expr: NonVirtualFunctionCall[V]) ⇒ + new IntraproceduralNonVirtualFunctionCallInterpreter( + cfg, this + ).interpret(expr, defSite) + case Assignment(_, _, expr: GetField[V]) ⇒ + new IntraproceduralFieldInterpreter(cfg, this).interpret(expr, defSite) + case ExprStmt(_, expr: VirtualFunctionCall[V]) ⇒ + new IntraproceduralVirtualFunctionCallInterpreter( + cfg, this + ).interpret(expr, defSite) + case ExprStmt(_, expr: StaticFunctionCall[V]) ⇒ + new IntraproceduralStaticFunctionCallInterpreter(cfg, this).interpret(expr, defSite) + case vmc: VirtualMethodCall[V] ⇒ + new IntraproceduralVirtualMethodCallInterpreter(cfg, this).interpret(vmc, defSite) + case nvmc: NonVirtualMethodCall[V] ⇒ + new IntraproceduralNonVirtualMethodCallInterpreter( + cfg, this + ).interpret(nvmc, defSite) + case _ ⇒ FinalEP(e, StringConstancyProperty.getNeutralElement) + } + result + } + +} + +object IntraproceduralInterpretationHandler { + + /** + * @see [[IntraproceduralInterpretationHandler]] + */ + def apply( + tac: TACode[TACMethodParameter, DUVar[ValueInformation]] + ): IntraproceduralInterpretationHandler = new IntraproceduralInterpretationHandler(tac) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralNonVirtualFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralNonVirtualFunctionCallInterpreter.scala new file mode 100644 index 0000000000..13c2ef3f8c --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralNonVirtualFunctionCallInterpreter.scala @@ -0,0 +1,40 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.NonVirtualFunctionCall +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter + +/** + * The `IntraproceduralNonVirtualFunctionCallInterpreter` is responsible for processing + * [[NonVirtualFunctionCall]]s in an intraprocedural fashion. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntraproceduralNonVirtualFunctionCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: IntraproceduralInterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = NonVirtualFunctionCall[V] + + /** + * This function always returns a result that contains [[StringConstancyProperty.lb]]. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty.lb) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralNonVirtualMethodCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralNonVirtualMethodCallInterpreter.scala new file mode 100644 index 0000000000..3868f54cf3 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralNonVirtualMethodCallInterpreter.scala @@ -0,0 +1,84 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.NonVirtualMethodCall +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter + +/** + * The `IntraproceduralNonVirtualMethodCallInterpreter` is responsible for processing + * [[NonVirtualMethodCall]]s in an intraprocedural fashion. + * For supported method calls, see the documentation of the `interpret` function. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntraproceduralNonVirtualMethodCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: IntraproceduralInterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = NonVirtualMethodCall[V] + + /** + * Currently, this function supports the interpretation of the following non virtual methods: + *

      + *
    • + * `<init>`, when initializing an object (for this case, currently zero constructor or + * one constructor parameter are supported; if more params are available, only the very first + * one is interpreted). + *
    • + *
    + * + * For all other calls, a result containing [[StringConstancyProperty.getNeutralElement]] will + * be returned. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret( + instr: NonVirtualMethodCall[V], defSite: Int + ): EOptionP[Entity, StringConstancyProperty] = { + val prop = instr.name match { + case "" ⇒ interpretInit(instr) + case _ ⇒ StringConstancyProperty.getNeutralElement + } + FinalEP(instr, prop) + } + + /** + * Processes an `<init>` method call. If it has no parameters, + * [[StringConstancyProperty.getNeutralElement]] will be returned. Otherwise, only the very + * first parameter will be evaluated and its result returned (this is reasonable as both, + * [[StringBuffer]] and [[StringBuilder]], have only constructors with <= 1 arguments and only + * these are currently interpreted). + */ + private def interpretInit(init: NonVirtualMethodCall[V]): StringConstancyProperty = { + init.params.size match { + case 0 ⇒ StringConstancyProperty.getNeutralElement + case _ ⇒ + val scis = ListBuffer[StringConstancyInformation]() + init.params.head.asVar.definedBy.foreach { ds ⇒ + val r = exprHandler.processDefSite(ds).asFinal + scis.append( + r.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + ) + } + val reduced = StringConstancyInformation.reduceMultiple(scis) + StringConstancyProperty(reduced) + } + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralStaticFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralStaticFunctionCallInterpreter.scala new file mode 100644 index 0000000000..5e565d966a --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralStaticFunctionCallInterpreter.scala @@ -0,0 +1,42 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.StaticFunctionCall +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter + +/** + * The `IntraproceduralStaticFunctionCallInterpreter` is responsible for processing + * [[StaticFunctionCall]]s in an intraprocedural fashion. + *

    + * For supported method calls, see the documentation of the `interpret` function. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntraproceduralStaticFunctionCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: IntraproceduralInterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = StaticFunctionCall[V] + + /** + * This function always returns a result containing [[StringConstancyProperty.lb]]. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = + FinalEP(instr, StringConstancyProperty.lb) + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralVirtualFunctionCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralVirtualFunctionCallInterpreter.scala new file mode 100644 index 0000000000..0cc3b663aa --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralVirtualFunctionCallInterpreter.scala @@ -0,0 +1,215 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.ComputationalTypeFloat +import org.opalj.br.ComputationalTypeInt +import org.opalj.br.ObjectType +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.br.ComputationalTypeDouble +import org.opalj.br.DoubleType +import org.opalj.br.FloatType +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * The `IntraproceduralVirtualFunctionCallInterpreter` is responsible for processing + * [[VirtualFunctionCall]]s in an intraprocedural fashion. + * The list of currently supported function calls can be seen in the documentation of [[interpret]]. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntraproceduralVirtualFunctionCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: IntraproceduralInterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = VirtualFunctionCall[V] + + /** + * Currently, this implementation supports the interpretation of the following function calls: + *

      + *
    • `append`: Calls to the `append` function of [[StringBuilder]] and [[StringBuffer]].
    • + *
    • + * `toString`: Calls to the `append` function of [[StringBuilder]] and [[StringBuffer]]. As + * a `toString` call does not change the state of such an object, an empty list will be + * returned. + *
    • + *
    • + * `replace`: Calls to the `replace` function of [[StringBuilder]] and [[StringBuffer]]. For + * further information how this operation is processed, see + * [[IntraproceduralVirtualFunctionCallInterpreter.interpretReplaceCall]]. + *
    • + *
    • + * Apart from these supported methods, a list with [[StringConstancyProperty.lb]] + * will be returned in case the passed method returns a [[java.lang.String]]. + *
    • + *
    + * + * If none of the above-described cases match, a result containing + * [[StringConstancyProperty.getNeutralElement]] will be returned. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val property = instr.name match { + case "append" ⇒ interpretAppendCall(instr) + case "toString" ⇒ interpretToStringCall(instr) + case "replace" ⇒ interpretReplaceCall(instr) + case _ ⇒ + instr.descriptor.returnType match { + case obj: ObjectType if obj.fqn == "java/lang/String" ⇒ + StringConstancyProperty.lb + case FloatType | DoubleType ⇒ + StringConstancyProperty(StringConstancyInformation( + StringConstancyLevel.DYNAMIC, + StringConstancyType.APPEND, + StringConstancyInformation.FloatValue + )) + case _ ⇒ StringConstancyProperty.getNeutralElement + } + } + + FinalEP(instr, property) + } + + /** + * Function for processing calls to [[StringBuilder#append]] or [[StringBuffer#append]]. Note + * that this function assumes that the given `appendCall` is such a function call! Otherwise, + * the expected behavior cannot be guaranteed. + */ + private def interpretAppendCall( + appendCall: VirtualFunctionCall[V] + ): StringConstancyProperty = { + val receiverSci = receiverValuesOfAppendCall(appendCall).stringConstancyInformation + val appendSci = valueOfAppendCall(appendCall).stringConstancyInformation + + // The case can occur that receiver and append value are empty; although, it is + // counter-intuitive, this case may occur if both, the receiver and the parameter, have been + // processed before + val sci = if (receiverSci.isTheNeutralElement && appendSci.isTheNeutralElement) { + StringConstancyInformation.getNeutralElement + } // It might be that we have to go back as much as to a New expression. As they do not + // produce a result (= empty list), the if part + else if (receiverSci.isTheNeutralElement) { + appendSci + } // The append value might be empty, if the site has already been processed (then this + // information will come from another StringConstancyInformation object + else if (appendSci.isTheNeutralElement) { + receiverSci + } // Receiver and parameter information are available => Combine them + else { + StringConstancyInformation( + StringConstancyLevel.determineForConcat( + receiverSci.constancyLevel, appendSci.constancyLevel + ), + StringConstancyType.APPEND, + receiverSci.possibleStrings + appendSci.possibleStrings + ) + } + + StringConstancyProperty(sci) + } + + /** + * This function determines the current value of the receiver object of an `append` call. + */ + private def receiverValuesOfAppendCall( + call: VirtualFunctionCall[V] + ): StringConstancyProperty = { + // There might be several receivers, thus the map; from the processed sites, however, use + // only the head as a single receiver interpretation will produce one element + val scis = call.receiver.asVar.definedBy.toArray.sorted.map { ds ⇒ + val r = exprHandler.processDefSite(ds) + r.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + }.filter { sci ⇒ !sci.isTheNeutralElement } + val sci = if (scis.isEmpty) StringConstancyInformation.getNeutralElement else + scis.head + StringConstancyProperty(sci) + } + + /** + * Determines the (string) value that was passed to a `String{Builder, Buffer}#append` method. + * This function can process string constants as well as function calls as argument to append. + */ + private def valueOfAppendCall( + call: VirtualFunctionCall[V] + ): StringConstancyProperty = { + val param = call.params.head.asVar + // .head because we want to evaluate only the first argument of append + val defSiteHead = param.definedBy.head + var r = exprHandler.processDefSite(defSiteHead) + var value = r.asFinal.p.asInstanceOf[StringConstancyProperty] + // If defSiteHead points to a New, value will be the empty list. In that case, process + // the first use site (which is the call) + if (value.isTheNeutralElement) { + r = exprHandler.processDefSite( + cfg.code.instructions(defSiteHead).asAssignment.targetVar.usedBy.toArray.min + ).asFinal + value = r.asFinal.p.asInstanceOf[StringConstancyProperty] + } + + val sci = value.stringConstancyInformation + val finalSci = param.value.computationalType match { + // For some types, we know the (dynamic) values + case ComputationalTypeInt ⇒ + // The value was already computed above; however, we need to check whether the + // append takes an int value or a char (if it is a constant char, convert it) + if (call.descriptor.parameterType(0).isCharType && + sci.constancyLevel == StringConstancyLevel.CONSTANT) { + sci.copy( + possibleStrings = sci.possibleStrings.toInt.toChar.toString + ) + } else { + sci + } + case ComputationalTypeFloat | ComputationalTypeDouble ⇒ + if (sci.constancyLevel == StringConstancyLevel.CONSTANT) { + sci + } else { + InterpretationHandler.getConstancyInfoForDynamicFloat + } + // Otherwise, try to compute + case _ ⇒ + sci + } + + StringConstancyProperty(finalSci) + } + + /** + * Function for processing calls to [[StringBuilder#toString]] or [[StringBuffer#toString]]. + * Note that this function assumes that the given `toString` is such a function call! Otherwise, + * the expected behavior cannot be guaranteed. + */ + private def interpretToStringCall( + call: VirtualFunctionCall[V] + ): StringConstancyProperty = { + val finalEP = exprHandler.processDefSite(call.receiver.asVar.definedBy.head).asFinal + finalEP.p.asInstanceOf[StringConstancyProperty] + } + + /** + * Function for processing calls to [[StringBuilder#replace]] or [[StringBuffer#replace]]. + * (Currently, this function simply approximates `replace` functions by returning the lower + * bound of [[StringConstancyProperty]]). + */ + private def interpretReplaceCall( + instr: VirtualFunctionCall[V] + ): StringConstancyProperty = InterpretationHandler.getStringConstancyPropertyForReplace + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralVirtualMethodCallInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralVirtualMethodCallInterpreter.scala new file mode 100644 index 0000000000..f35fbb332f --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/interpretation/intraprocedural/IntraproceduralVirtualMethodCallInterpreter.scala @@ -0,0 +1,62 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.FinalEP +import org.opalj.br.cfg.CFG +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringConstancyLevel +import org.opalj.br.fpcf.properties.string_definition.StringConstancyType +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.VirtualMethodCall +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.AbstractStringInterpreter + +/** + * The `IntraproceduralVirtualMethodCallInterpreter` is responsible for processing + * [[VirtualMethodCall]]s in an intraprocedural fashion. + * For supported method calls, see the documentation of the `interpret` function. + * + * @see [[AbstractStringInterpreter]] + * + * @author Patrick Mell + */ +class IntraproceduralVirtualMethodCallInterpreter( + cfg: CFG[Stmt[V], TACStmts[V]], + exprHandler: IntraproceduralInterpretationHandler +) extends AbstractStringInterpreter(cfg, exprHandler) { + + override type T = VirtualMethodCall[V] + + /** + * Currently, this function supports the interpretation of the following virtual methods: + *
      + *
    • + * `setLength`: `setLength` is a method to reset / clear a [[StringBuilder]] / [[StringBuffer]] + * (at least when called with the argument `0`). For simplicity, this interpreter currently + * assumes that 0 is always passed, i.e., the `setLength` method is currently always regarded as + * a reset mechanism. + *
    • + *
    + * + * For all other calls, a result containing [[StringConstancyProperty.getNeutralElement]] will + * be returned. + * + * @note For this implementation, `defSite` does not play a role. + * + * @see [[AbstractStringInterpreter.interpret]] + */ + override def interpret(instr: T, defSite: Int): EOptionP[Entity, StringConstancyProperty] = { + val sci = instr.name match { + case "setLength" ⇒ StringConstancyInformation( + StringConstancyLevel.CONSTANT, StringConstancyType.RESET + ) + case _ ⇒ StringConstancyInformation.getNeutralElement + } + FinalEP(instr, StringConstancyProperty(sci)) + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/AbstractPathFinder.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/AbstractPathFinder.scala new file mode 100644 index 0000000000..a8d094f7ce --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/AbstractPathFinder.scala @@ -0,0 +1,1307 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.preprocessing + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.br.cfg.BasicBlock +import org.opalj.br.cfg.CatchNode +import org.opalj.br.cfg.CFG +import org.opalj.br.cfg.CFGNode +import org.opalj.tac.Goto +import org.opalj.tac.If +import org.opalj.tac.ReturnValue +import org.opalj.tac.Stmt +import org.opalj.tac.Switch +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V + +/** + * [[AbstractPathFinder]] provides a scaffolding for finding all relevant paths in a CFG in the + * scope of string definition analyses. + * + * @param cfg The control flow graph (CFG) on which instance of this class will operate on. + * + * @author Patrick Mell + */ +abstract class AbstractPathFinder(cfg: CFG[Stmt[V], TACStmts[V]]) { + + /** + * CSInfo stores information regarding control structures (CS) in the form: Index of the start + * statement of that CS, index of the end statement of that CS and the type. + */ + protected type CSInfo = (Int, Int, NestedPathType.Value) + + /** + * Represents control structures in a hierarchical order. The top-most level of the hierarchy + * has no [[CSInfo]], thus value can be set to `None`; all other elements are required to have + * that value set! + * + * @param hierarchy A list of pairs where the first element represents the parent and the second + * the list of children. As the list of children is of type + * [[HierarchicalCSOrder]], too, this creates a recursive structure. + * If two elements, ''e1'' and ''e2'', are on the same hierarchy level neither + * ''e1'' is a parent or child of ''e'' and nor is ''e2'' a parent or child of + * ''e1''. + */ + protected case class HierarchicalCSOrder( + hierarchy: List[(Option[CSInfo], List[HierarchicalCSOrder])] + ) + + /** + * Determines the bounds of a conditional with alternative (like an `if-else` or a `switch` with + * a `default` case, that is the indices of the first and the last statement belonging to the + * whole block (i.e., for an `if-else` this function returns the index of the very first + * statement of the `if`, including the branching site, as the first value and the index of the + * very last element of the `else` part as the second value). + * + * @param branchingSite The `branchingSite` is supposed to point at the very first `if` of the + * conditional. + * @param processedIfs A map which will be filled with the `if` statements that will be + * encountered during the processing. This might be relevant for a method + * processing all `if`s - the `if` of an `else-if` is shall probably be + * processed only once. This map can be used for that purpose. + * @return Returns the index of the start statement and the index of the end statement of the + * whole conditional as described above. + */ + private def getStartAndEndIndexOfCondWithAlternative( + branchingSite: Int, processedIfs: mutable.Map[Int, Unit.type] + ): (Int, Int) = { + processedIfs(branchingSite) = Unit + + var endSite = -1 + val stack = mutable.Stack[Int](branchingSite) + while (stack.nonEmpty) { + val popped = stack.pop() + val nextBlock = cfg.bb(popped).successors.map { + case bb: BasicBlock ⇒ bb.startPC + // Handle Catch Nodes? + case _ ⇒ -1 + }.max + var containsIf = false + for (i ← cfg.bb(nextBlock).startPC.to(cfg.bb(nextBlock).endPC)) { + if (cfg.code.instructions(i).isInstanceOf[If[V]]) { + processedIfs(i) = Unit + containsIf = true + } + } + + if (containsIf) { + stack.push(nextBlock) + } else { + // Check and find if there is a goto which provides further information about the + // bounds of the conditional; a goto is relevant, if it does not point back at a + // surrounding loop + var isRelevantGoto = false + val relevantGoTo: Option[Goto] = cfg.code.instructions(nextBlock - 1) match { + case goto: Goto ⇒ + // A goto is not relevant if it points at a loop that is within the + // conditional (this does not help / provides no further information) + val gotoSite = goto.targetStmt + isRelevantGoto = !cfg.findNaturalLoops().exists { l ⇒ + l.head == gotoSite + } + Some(goto) + case _ ⇒ None + } + + relevantGoTo match { + case Some(goto) ⇒ + if (isRelevantGoto) { + // Find the goto that points after the "else" part (the assumption is + // that this goto is the very last element of the current branch + endSite = goto.targetStmt + // The goto might point back at the beginning of a loop; if so, the end + // of the if/else is denoted by the end of the loop + if (endSite < branchingSite) { + endSite = cfg.findNaturalLoops().filter(_.head == endSite).head.last + } else { + endSite -= 1 + } + } else { + // If the conditional is encloses in a try-catch block, consider this + // bounds and otherwise the bounds of the surrounding element + cfg.bb(nextBlock).successors.find(_.isInstanceOf[CatchNode]) match { + case Some(cs: CatchNode) ⇒ + endSite = cs.endPC + if (endSite == -1) { + endSite = nextBlock + } + case _ ⇒ + endSite = if (nextBlock > branchingSite) nextBlock - 1 else + cfg.findNaturalLoops().find { + _.head == goto.targetStmt + }.get.last + } + } + case _ ⇒ + // No goto available => Jump after next block + var nextIf: Option[If[V]] = None + var i = nextBlock + while (i < cfg.code.instructions.length && nextIf.isEmpty) { + cfg.code.instructions(i) match { + case iff: If[V] ⇒ + nextIf = Some(iff) + processedIfs(i) = Unit + case _ ⇒ + } + i += 1 + } + endSite = if (nextIf.isDefined) nextIf.get.targetStmt else { + stack.clear() + i - 1 + } + } + } + if (endSite < branchingSite) { + endSite = nextBlock + } + } + + (branchingSite, endSite) + } + + /** + * Determines the bounds of a conditional without alternative (like an `if-else-if` or a + * `switch` without a `default` case, that is the indices of the first and the last statement + * belonging to the whole block (i.e., for an `if-else-if` this function returns the index of + * the very first statement of the `if`, including the branching site, as the first value and + * the index of the very last element of the `else if` part as the second value). + * + * @param branchingSite The `branchingSite` is supposed to point at the very first `if` of the + * conditional. + * @param processedIfs A map which will be filled with the `if` statements that will be + * encountered during the processing. This might be relevant for a method + * processing all `if`s - the `if` of an `else-if` is shall probably be + * processed only once. This map can be used for that purpose. + * @return Returns the index of the start statement and the index of the end statement of the + * whole conditional as described above. + */ + private def getStartAndEndIndexOfCondWithoutAlternative( + branchingSite: Int, processedIfs: mutable.Map[Int, Unit.type] + ): (Int, Int) = { + // Find the index of very last element in the if block (here: The goto element; is it always + // present?) + val nextPossibleIfBlock = cfg.bb(branchingSite).successors.map { + case bb: BasicBlock ⇒ bb.startPC + // Handle Catch Nodes? + case _ ⇒ -1 + }.max + + var nextIfIndex = -1 + val ifTarget = cfg.code.instructions(branchingSite).asInstanceOf[If[V]].targetStmt + for (i ← cfg.bb(nextPossibleIfBlock).startPC.to(cfg.bb(nextPossibleIfBlock).endPC)) { + // The second condition is necessary to detect two consecutive "if"s (not in an else-if + // relation) + if (cfg.code.instructions(i).isInstanceOf[If[V]] && ifTarget != i) { + nextIfIndex = i + } + } + + var endIndex = nextPossibleIfBlock - 1 + if (nextIfIndex > -1 && !isHeadOfLoop(nextIfIndex, cfg.findNaturalLoops(), cfg)) { + processedIfs(nextIfIndex) = Unit + val (_, newEndIndex) = getStartAndEndIndexOfCondWithoutAlternative( + nextIfIndex, processedIfs + ) + endIndex = newEndIndex + } + + // It might be that the "i"f is the very last element in a loop; in this case, it is a + // little bit more complicated to find the end of the "if": Go up to the element that points + // to the if target element + if (ifTarget < branchingSite) { + val seenElements: mutable.Map[Int, Unit] = mutable.Map() + val toVisit = mutable.Stack[Int](branchingSite) + while (toVisit.nonEmpty) { + val popped = toVisit.pop() + seenElements(popped) = Unit + val relevantSuccessors = cfg.bb(popped).successors.filter { + _.isInstanceOf[BasicBlock] + }.map(_.asBasicBlock) + if (relevantSuccessors.size == 1 && relevantSuccessors.head.startPC == ifTarget) { + endIndex = cfg.bb(popped).endPC + toVisit.clear() + } else { + toVisit.pushAll(relevantSuccessors.filter { s ⇒ + s.nodeId != ifTarget && !seenElements.contains(s.nodeId) + }.map(_.startPC)) + } + } + } + + // It might be that this conditional is within a try block. In that case, endIndex will + // point after all catch clauses which is to much => narrow down to try block + val inTryBlocks = cfg.catchNodes.filter { cn ⇒ + branchingSite >= cn.startPC && branchingSite <= cn.endPC + } + if (inTryBlocks.nonEmpty) { + val tryEndPC = inTryBlocks.minBy(-_.startPC).endPC + if (endIndex > tryEndPC) { + endIndex = tryEndPC + } + } + + // It is now necessary to collect all ifs that belong to the whole if condition (in the + // high-level construct) + cfg.bb(ifTarget).predecessors.foreach { + case pred: BasicBlock ⇒ + for (i ← pred.startPC.to(pred.endPC)) { + if (cfg.code.instructions(i).isInstanceOf[If[V]]) { + processedIfs(i) = Unit + } + } + // How about CatchNodes? + case _ ⇒ + } + + (branchingSite, endIndex) + } + + /** + * This method finds the very first return value after (including) the given start position. + * + * @param startPos The index of the position to start with. + * @return Returns either the index of the very first found [[ReturnValue]] or the index of the + * very last statement within the instructions if no [[ReturnValue]] could be found. + */ + private def findNextReturn(startPos: Int): Int = { + var returnPos = startPos + var foundReturn = false + while (!foundReturn && returnPos < cfg.code.instructions.length) { + if (cfg.code.instructions(returnPos).isInstanceOf[ReturnValue[V]]) { + foundReturn = true + } else { + returnPos += 1 + } + } + returnPos + } + + /** + * This function detects all `try-catch` blocks in the given CFG, extracts the indices of the + * first statement for each `try` as the as well as the indices of the last statements of the + * `try-catch` blocks and returns these pairs (along with [[NestedPathType.TryCatchFinally]]. + * + * @return Returns information on all `try-catch` blocks present in the given `cfg`. + * + * @note The bounds, which are determined by this function do not include the `finally` part of + * `try` blocks (but for the `catch` blocks). Thus, a function processing the result of + * this function can either add the `finally` to the `try` block (and keep it in the + * `catch` block(s)) or add it after the whole `try-catch` but disregards it for all + * `catch` blocks. + * @note This function has basic support for `throwable`s. + */ + private def determineTryCatchBounds(): List[CSInfo] = { + // Stores the startPC as key and the index of the end of a catch (or finally if it is + // present); a map is used for faster accesses + val tryInfo = mutable.Map[Int, Int]() + + cfg.catchNodes.foreach { cn ⇒ + if (!tryInfo.contains(cn.startPC)) { + val cnSameStartPC = cfg.catchNodes.filter(_.startPC == cn.startPC) + val hasCatchFinally = cnSameStartPC.exists(_.catchType.isEmpty) + val hasOnlyFinally = cnSameStartPC.size == 1 && hasCatchFinally + val isThrowable = cn.catchType.isDefined && + cn.catchType.get.fqn == "java/lang/Throwable" + // When there is a throwable involved, it might be the case that there is only one + // element in cnSameStartPC, the finally part; do not process it now (but in another + // catch node) + if (!hasOnlyFinally) { + if (isThrowable) { + val throwFinally = cfg.catchNodes.find(_.startPC == cn.handlerPC) + val endIndex = if (throwFinally.isDefined) throwFinally.get.endPC - 1 else + cn.endPC - 1 + tryInfo(cn.startPC) = endIndex + } // If there is only one CatchNode for a startPC, i.e., no finally, no other + // catches, the end index can be directly derived from the successors + else if (cnSameStartPC.tail.isEmpty && !isThrowable) { + if (cn.endPC > -1) { + var end = cfg.bb(cn.endPC).successors.map { + case bb: BasicBlock ⇒ bb.startPC - 1 + case _ ⇒ -1 + }.max + if (end == -1) { + end = findNextReturn(cn.handlerPC) + } + tryInfo(cn.startPC) = end + } // -1 might be the case if the catch returns => Find that return and use + // it as the end of the range + else { + findNextReturn(cn.handlerPC) + } + } // Otherwise, the index after the try and all catches marks the end index (-1 + // to not already get the start index of the successor) + else { + if (hasCatchFinally) { + // Find out, how many elements the finally block has and adjust the try + // block accordingly + val startFinally = cnSameStartPC.map(_.handlerPC).max + val endFinally = cfg.code.instructions(startFinally - 1) match { + // If the finally does not terminate a method, it has a goto to jump + // after the finally block; if not, the end of the finally is marked + // by the end of the method + case Goto(_, target) ⇒ target + case _ ⇒ cfg.code.instructions.length - 1 + } + val numElementsFinally = endFinally - startFinally - 1 + val endOfFinally = cnSameStartPC.map(_.handlerPC).max + tryInfo(cn.startPC) = endOfFinally - 1 - numElementsFinally + } else { + val blockIndex = if (cnSameStartPC.head.endPC < 0) + cfg.code.instructions.length - 1 else cnSameStartPC.head.endPC + tryInfo(cn.startPC) = cfg.bb(blockIndex).successors.map { + case bb: BasicBlock ⇒ bb.startPC + case _ ⇒ blockIndex + }.max - 1 + } + } + } + } + } + + tryInfo.map { + case (key, value) ⇒ (key, value, NestedPathType.TryCatchFinally) + }.toList + } + + /** + * This function serves as a helper / accumulator function that builds the recursive hierarchy + * for a given element. + * + * @param element The element for which a hierarchy is to be built. + * @param children Maps from parent elements ([[CSInfo]]) to its children. `children` is + * supposed to contain all known parent-children relations in order to guarantee + * that the recursive calls will produce a correct result as well). + * @return The hierarchical structure for `element`. + */ + private def buildHierarchy( + element: CSInfo, + children: mutable.Map[CSInfo, ListBuffer[CSInfo]] + ): HierarchicalCSOrder = { + if (!children.contains(element)) { + // Recursion anchor (no children available + HierarchicalCSOrder(List((Some(element), List()))) + } else { + HierarchicalCSOrder(List(( + Some(element), children(element).map { buildHierarchy(_, children) }.toList + ))) + } + } + + /** + * This function builds a [[Path]] that consists of a single [[NestedPathElement]] of type + * [[NestedPathType.Repetition]]. If `fill` is set to `true`, the nested path element will be + * filled with [[FlatPathElement]] ranging from `start` to `end` (otherwise, the nested path + * element remains empty and is to be filled outside this method). + * This method returns the [[Path]] element along with a list of a single element that consists + * of the tuple `(start, end)`. + */ + private def buildRepetitionPath( + start: Int, end: Int, fill: Boolean + ): (Path, List[(Int, Int)]) = { + val path = ListBuffer[SubPath]() + if (fill) { + start.to(end).foreach(i ⇒ path.append(FlatPathElement(i))) + } + (Path(List(NestedPathElement(path, Some(NestedPathType.Repetition)))), List((start, end))) + } + + /** + * This function builds the [[Path]] element for conditionals with and without alternatives + * (e.g., `if`s that have an `else` block or not); which one is determined by `pathType`. + * `start` and `end` determine the start and end index of the conditional (`start` is supposed + * to contain the initial branching site of the conditionals). + * This function determines all `if`, `else-if`, and `else` blocks and adds them to the path + * element that will be returned. If `fill` is set to `true`, the different parts will be filled + * with [[FlatPathElement]]s. + * For example, assume an `if-else` where the `if` start at index 5, ends at index 10, and the + * `else` part starts at index 11 and ends at index 20. [[Path]] will then contain a + * [[NestedPathElement]] of type [[NestedPathType.CondWithAlternative]] with two children. If + * `fill` equals `true`, the first inner path will contain flat path elements from 5 to 10 and + * the second from 11 to 20. + */ + private def buildCondPath( + start: Int, end: Int, pathType: NestedPathType.Value, fill: Boolean + ): (Path, List[(Int, Int)]) = { + // Stores the start and end indices of the parts that form the if-(else-if)*-else, i.e., if + // there is an if-else construct, startEndPairs contains two elements: 1) The start index of + // the if, the end index of the if part and 2) the start index of the else part and the end + // index of the else part + val startEndPairs = ListBuffer[(Int, Int)]() + + var endSite = -1 + val stack = mutable.Stack[Int](start) + while (stack.nonEmpty) { + val popped = stack.pop() + if (popped <= end) { + var nextBlock = cfg.bb(popped).successors.map { + case bb: BasicBlock ⇒ bb.startPC + // Handle Catch Nodes? + case _ ⇒ -1 + }.max + + if (pathType == NestedPathType.CondWithAlternative && nextBlock > end) { + nextBlock = popped + 1 + while (nextBlock < cfg.code.instructions.length - 1 && + !cfg.code.instructions(nextBlock).isInstanceOf[If[V]]) { + nextBlock += 1 + } + } + + var containsIf = false + for (i ← cfg.bb(nextBlock).startPC.to(cfg.bb(nextBlock).endPC)) { + if (cfg.code.instructions(i).isInstanceOf[If[V]]) { + containsIf = true + } + } + + if (containsIf) { + startEndPairs.append((popped, nextBlock - 1)) + stack.push(nextBlock) + } else { + if (popped <= end) { + endSite = nextBlock - 1 + if (endSite == start) { + endSite = end + } // The following is necessary to not exceed bounds (might be the case + // within a try block for example) + else if (endSite > end) { + endSite = end + } + startEndPairs.append((popped, endSite)) + } + } + } + } + + // Append the "else" branch (if present) + if (pathType == NestedPathType.CondWithAlternative && startEndPairs.last._2 + 1 <= end) { + startEndPairs.append((startEndPairs.last._2 + 1, end)) + } + + val subPaths = ListBuffer[SubPath]() + startEndPairs.foreach { + case (startSubpath, endSubpath) ⇒ + val subpathElements = ListBuffer[SubPath]() + if (fill) { + subpathElements.appendAll(startSubpath.to(endSubpath).map(FlatPathElement)) + } + if (!fill || subpathElements.nonEmpty) + subPaths.append(NestedPathElement(subpathElements, None)) + } + + val pathTypeToUse = if (pathType == NestedPathType.CondWithAlternative && + startEndPairs.length == 1) NestedPathType.CondWithoutAlternative else pathType + + (Path(List(NestedPathElement(subPaths, Some(pathTypeToUse)))), startEndPairs.toList) + } + + /** + * This function works analogously to [[buildCondPath]] only that it processes [[Switch]] + * statements and that it determines itself whether the switch contains a default case or not. + */ + private def buildPathForSwitch( + start: Int, end: Int, fill: Boolean + ): (Path, List[(Int, Int)]) = { + val startEndPairs = ListBuffer[(Int, Int)]() + val switch = cfg.code.instructions(start).asSwitch + val caseStmts = ListBuffer[Int](switch.caseStmts.sorted: _*) + + val containsDefault = caseStmts.length == caseStmts.distinct.length + if (containsDefault) { + caseStmts.append(switch.defaultStmt) + } + val pathType = if (containsDefault) NestedPathType.CondWithAlternative else + NestedPathType.CondWithoutAlternative + + var previousStart = caseStmts.head + caseStmts.tail.foreach { nextStart ⇒ + val currentEnd = nextStart - 1 + if (currentEnd >= previousStart) { + startEndPairs.append((previousStart, currentEnd)) + } + previousStart = nextStart + } + if (previousStart <= end) { + startEndPairs.append((previousStart, end)) + } + + val subPaths = ListBuffer[SubPath]() + startEndPairs.foreach { + case (startSubpath, endSubpath) ⇒ + val subpathElements = ListBuffer[SubPath]() + subPaths.append(NestedPathElement(subpathElements, None)) + if (fill) { + subpathElements.appendAll(startSubpath.to(endSubpath).map(FlatPathElement)) + } + } + (Path(List(NestedPathElement(subPaths, Some(pathType)))), startEndPairs.toList) + } + + /** + * This function works analogously to [[buildCondPath]], i.e., it determines the start and end + * index of the `catch` block and the start and end indices of the `catch` blocks (if present). + * + * @note Note that the built path has the following properties: The end index for the `try` + * block excludes the `finally` part if it is present; the same applies to the `catch` + * blocks! However, the `finally` block is inserted after the [[NestedPathElement]], i.e., + * the path produced by this function contains more than one element (if a `finally` + * block is present; this is handled by this function as well). + * + * @note This function has basic / primitive support for `throwable`s. + */ + private def buildTryCatchPath( + start: Int, end: Int, fill: Boolean + ): (Path, List[(Int, Int)]) = { + // For a description, see the comment of this variable in buildCondPath + val startEndPairs = ListBuffer[(Int, Int)]() + + var catchBlockStartPCs = ListBuffer[Int]() + var hasFinallyBlock = false + var throwableElement: Option[CatchNode] = None + cfg.bb(start).successors.foreach { + case cn: CatchNode ⇒ + // Add once for the try block + if (startEndPairs.isEmpty) { + val endPC = if (cn.endPC >= 0) cn.endPC else cn.handlerPC + startEndPairs.append((cn.startPC, endPC)) + } + if (cn.catchType.isDefined && cn.catchType.get.fqn == "java/lang/Throwable") { + throwableElement = Some(cn) + } else { + catchBlockStartPCs.append(cn.handlerPC) + if (cn.startPC == start && cn.catchType.isEmpty) { + hasFinallyBlock = true + } + } + case _ ⇒ + } + + if (throwableElement.isDefined) { + val throwCatch = cfg.catchNodes.find(_.startPC == throwableElement.get.handlerPC) + if (throwCatch.isDefined) { + // This is for the catch block + startEndPairs.append((throwCatch.get.startPC, throwCatch.get.endPC - 1)) + } + } else if (startEndPairs.nonEmpty) { + var numElementsFinally = 0 + if (hasFinallyBlock) { + // Find out, how many elements the finally block has + val startFinally = catchBlockStartPCs.max + val endFinally = cfg.code.instructions(startFinally - 1) match { + // If the finally does not terminate a method, it has a goto to jump + // after the finally block; if not, the end of the finally is marked + // by the end of the method + case Goto(_, target) ⇒ target + case _ ⇒ cfg.code.instructions.length - 1 + } + // -1 for unified processing further down below (because in + // catchBlockStartPCs.foreach, 1 is subtracted) + numElementsFinally = endFinally - startFinally - 1 + } else { + val endOfAfterLastCatch = cfg.bb(startEndPairs.head._2).successors.map { + case bb: BasicBlock ⇒ bb.startPC + case _ ⇒ -1 + }.max + catchBlockStartPCs.append(endOfAfterLastCatch) + } + + catchBlockStartPCs = catchBlockStartPCs.sorted + catchBlockStartPCs.zipWithIndex.foreach { + case (nextStart, i) ⇒ + if (i + 1 < catchBlockStartPCs.length) { + startEndPairs.append( + (nextStart, catchBlockStartPCs(i + 1) - 1 - numElementsFinally) + ) + } + } + } // In some cases (sometimes when a throwable is involved) the successors are no catch + // nodes => Find the bounds now + else { + val cn = cfg.catchNodes.filter(_.startPC == start).head + startEndPairs.append((cn.startPC, cn.endPC - 1)) + val endOfCatch = cfg.code.instructions(cn.handlerPC - 1) match { + case goto: Goto ⇒ + // The first statement after the catches; it might be less than cn.startPC in + // case it refers to a loop. If so, use the "if" to find the end + var indexFirstAfterCatch = goto.targetStmt + if (indexFirstAfterCatch < cn.startPC) { + var iff: Option[If[V]] = None + var i = indexFirstAfterCatch + while (iff.isEmpty) { + cfg.code.instructions(i) match { + case foundIf: If[V] ⇒ iff = Some(foundIf) + case _ ⇒ + } + i += 1 + } + indexFirstAfterCatch = iff.get.targetStmt + } + indexFirstAfterCatch + case _ ⇒ findNextReturn(cn.handlerPC) + } + startEndPairs.append((cn.endPC, endOfCatch)) + } + + val subPaths = ListBuffer[SubPath]() + startEndPairs.foreach { + case (startSubpath, endSubpath) ⇒ + val subpathElements = ListBuffer[SubPath]() + subPaths.append(NestedPathElement(subpathElements, None)) + if (fill) { + subpathElements.appendAll(startSubpath.to(endSubpath).map(FlatPathElement)) + } + } + + // If there is a finally part, append everything after the end of the try block up to the + // very first catch block + if (hasFinallyBlock && fill) { + subPaths.appendAll((startEndPairs.head._2 + 1).until(startEndPairs(1)._1).map { i ⇒ + FlatPathElement(i) + }) + } + + ( + Path(List(NestedPathElement(subPaths, Some(NestedPathType.TryCatchFinally)))), + startEndPairs.toList + ) + } + + /** + * Generates a new [[NestedPathElement]] with a given number of inner [[NestedPathElement]]s. + */ + protected def generateNestPathElement( + numInnerElements: Int, + elementType: NestedPathType.Value + ): NestedPathElement = { + val outerNested = NestedPathElement(ListBuffer(), Some(elementType)) + for (_ ← 0.until(numInnerElements)) { + outerNested.element.append(NestedPathElement(ListBuffer(), None)) + } + outerNested + } + + /** + * Determines whether a given `site` is the head of a loop by comparing it to a set of loops + * (here a list of lists). This function returns ''true'', if `site` is the head of one of the + * inner lists. + * Note that some high-level constructs, such as ''while-true'', might produce a loop where the + * check, whether to loop again or leave the loop, is placed at the end of the loop. In such + * cases, the very first statement of a loop is considered its head (which can be an assignment + * or function call not related to the loop header for instance). + */ + protected def isHeadOfLoop( + site: Int, loops: List[List[Int]], cfg: CFG[Stmt[V], TACStmts[V]] + ): Boolean = { + var belongsToLoopHeader = false + + // First, check the trivial case: Is the given site the first statement in a loop (covers, + // e.g., the above-mentioned while-true cases) + loops.foreach { loop ⇒ + if (!belongsToLoopHeader) { + if (loop.head == site) { + belongsToLoopHeader = true + } + } + } + + // The loop header might not only consist of the very first element in 'loops'; thus, check + // whether the given site is between the first site of a loop and the site of the very first + // 'if' (again, respect structures as produces by while-true loops) + if (!belongsToLoopHeader) { + loops.foreach { nextLoop ⇒ + if (!belongsToLoopHeader) { + val start = nextLoop.head + var end = start + while (!cfg.code.instructions(end).isInstanceOf[If[V]]) { + end += 1 + } + if (site >= start && site <= end && end < nextLoop.last) { + belongsToLoopHeader = true + } + } + } + } + belongsToLoopHeader + } + + /** + * Determines whether a given `site` is the end of a loop by comparing it to a set of loops + * (here a list of lists). This function returns ''true'', if `site` is the last element of one + * of the inner lists. + */ + protected def isEndOfLoop(site: Int, loops: List[List[Int]]): Boolean = + loops.foldLeft(false)((old: Boolean, nextLoop: List[Int]) ⇒ old || nextLoop.last == site) + + /** + * Checks whether a given [[BasicBlock]] has one (or several) successors which have at least n + * predecessors. + * + * @param bb The basic block to check whether it has a successor with at least n predecessors. + * @param n The number of required predecessors. + * @return Returns ''true'' if ''bb'' has a successor which has at least ''n'' predecessors. + * + * @note This function regards as successors and predecessors only [[BasicBlock]]s. + */ + protected def hasSuccessorWithAtLeastNPredecessors(bb: BasicBlock, n: Int = 2): Boolean = + bb.successors.filter( + _.isInstanceOf[BasicBlock] + ).foldLeft(false)((prev: Boolean, next: CFGNode) ⇒ { + prev || (next.predecessors.count(_.isInstanceOf[BasicBlock]) >= n) + }) + + /** + * This function checks if a branching corresponds to an if (or if-elseif) structure that has no + * else block. + * Currently, this function is implemented to check whether the very last element of the + * successors of the given site is a path past the if (or if-elseif) paths. + * + * @param branchingSite The site / index of a branching that is to be checked. + * @param cfg The control flow graph underlying the successors. + * @return Returns ''true'', if the very last element of the successors is a child of one of the + * other successors. If this is the case, the branching corresponds to one without an + * ''else'' branch. + */ + protected def isCondWithoutElse( + branchingSite: Int, + cfg: CFG[Stmt[V], TACStmts[V]], + processedIfs: mutable.Map[Int, Unit.type] + ): Boolean = { + val successorBlocks = cfg.bb(branchingSite).successors + // CatchNode exists => Regard it as conditional without alternative + if (successorBlocks.exists(_.isInstanceOf[CatchNode])) { + processedIfs(branchingSite) = Unit + return false + } + + val successors = successorBlocks.map(_.nodeId).toArray.sorted + + // In case, there is only one larger successor, this will be a condition without else + // (smaller indices might arise, e.g., when an "if" is the last part of a loop) + if (successors.count(_ > branchingSite) == 1) { + return true + } + + // Separate the last element from all previous ones + //val branches = successors.reverse.tail.reverse + val lastEle = successors.last + + // If an "if" ends at the end of a loop (the "if" must be within that loop!), it cannot have + // an else + val loopOption = cfg.findNaturalLoops().find(_.last == lastEle - 1) + if (loopOption.isDefined && loopOption.get.head < branchingSite) { + return true + } + + val indexIf = cfg.bb(lastEle) match { + case bb: BasicBlock ⇒ + val ifPos = bb.startPC.to(bb.endPC).filter( + cfg.code.instructions(_).isInstanceOf[If[V]] + ) + if (ifPos.nonEmpty && !isHeadOfLoop(ifPos.head, cfg.findNaturalLoops(), cfg)) { + ifPos.head + } else { + -1 + } + case _ ⇒ -1 + } + + if (indexIf != -1) { + // For else-if constructs + isCondWithoutElse(indexIf, cfg, processedIfs) + } else { + // For every successor (except the very last one), execute a DFS to check whether the + // very last element is a successor. If so, this represents a path past the if (or + // if-elseif). + var reachableCount = successors.count(_ == lastEle) + successors.foreach { next ⇒ + val seenNodes = ListBuffer[CFGNode](cfg.bb(branchingSite), cfg.bb(next)) + val toVisitStack = mutable.Stack[CFGNode](cfg.bb(next).successors.toArray: _*) + while (toVisitStack.nonEmpty) { + val from = toVisitStack.pop() + val to = from.successors + if ((from.nodeId == lastEle || to.contains(cfg.bb(lastEle))) && + from.nodeId >= branchingSite) { + reachableCount += 1 + } + seenNodes.append(from) + toVisitStack.pushAll(to.filter(!seenNodes.contains(_))) + } + } + if (reachableCount > 1) { + true + } else { + processedIfs(branchingSite) = Unit + false + } + } + } + + /** + * Based on the member `cfg` of this instance, this function checks whether a path from node + * `from` to node `to` exists. If so, `true` is returned and `false otherwise`. Optionally, a + * list of `alreadySeen` elements can be passed which influences which paths are to be followed + * (when assembling a path ''p'' and the next node, ''n_p'' in ''p'', is a node that was already + * seen, the path will not be continued in the direction of ''n_p'' (but in other directions + * that are not in `alreadySeen`)). + * + * @note This function assumes that `from` >= 0! + */ + protected def doesPathExistTo( + from: Int, to: Int, alreadySeen: List[Int] = List() + ): Boolean = { + val stack = mutable.Stack(from) + val seenNodes = mutable.Map[Int, Unit]() + alreadySeen.foreach(seenNodes(_)= Unit) + seenNodes(from) = Unit + + while (stack.nonEmpty) { + val popped = stack.pop() + cfg.bb(popped).successors.foreach { nextBlock ⇒ + // -1 is okay, as this value will not be processed (due to the flag processBlock) + var startPC = -1 + var endPC = -1 + var processBlock = true + nextBlock match { + case bb: BasicBlock ⇒ + startPC = bb.startPC; endPC = bb.endPC + case cn: CatchNode ⇒ + startPC = cn.startPC; endPC = cn.endPC + case _ ⇒ processBlock = false + } + + if (processBlock) { + if (startPC >= to && endPC <= to) { + // When the `to` node was seen, immediately return + return true + } else if (!seenNodes.contains(startPC)) { + stack.push(startPC) + seenNodes(startPC) = Unit + } + } + } + } + + // When this part is reached, no path could be found + false + } + + /** + * Determines the bounds of a loop, that is the indices of the first and the last statement. + * + * @param index The index of the statement that is the `if` statement of the loop. This function + * can deal with `if`s within the loop header or loop footer. + * @return Returns the index of the very first statement of the loop as well as the index of the + * very last statement index. + */ + private def getStartAndEndIndexOfLoop(index: Int): (Int, Int) = { + var startIndex = -1 + var endIndex = -1 + val relevantLoop = cfg.findNaturalLoops().filter(nextLoop ⇒ + // The given index might belong either to the start or to the end of a loop + isHeadOfLoop(index, List(nextLoop), cfg) || isEndOfLoop(index, List(nextLoop))) + if (relevantLoop.nonEmpty) { + startIndex = relevantLoop.head.head + endIndex = relevantLoop.head.last + } + (startIndex, endIndex) + } + + /** + * This function determines the type of the [[If]] statement, i.e., an element of + * [[NestedPathType]] as well as the indices of the very first and very last statement that + * belong to the `if`. + * + * @param stmt The index of the statement to process. This statement must be of type [[If]]. + * @param processedIfs A map that serves as a look-up table to 1) determine which `if`s have + * already been processed (and thus will not be processed again), and 2) to + * extend this table by the `if`s encountered in this procedure. + * @return Returns the start index, end index, and type of the `if` in that order. + * + * @note For further details, see [[getStartAndEndIndexOfCondWithAlternative]], + * [[getStartAndEndIndexOfCondWithoutAlternative]], and [[determineTryCatchBounds]]. + */ + protected def processIf( + stmt: Int, processedIfs: mutable.Map[Int, Unit.type] + ): CSInfo = { + val csType = determineTypeOfIf(stmt, processedIfs) + val (startIndex, endIndex) = csType match { + case NestedPathType.Repetition ⇒ + processedIfs(stmt) = Unit + getStartAndEndIndexOfLoop(stmt) + case NestedPathType.CondWithoutAlternative ⇒ + getStartAndEndIndexOfCondWithoutAlternative(stmt, processedIfs) + // _ covers CondWithAlternative and TryCatchFinally, however, the latter one should + // never be present as the element referring to stmts is / should be an If + case _ ⇒ + getStartAndEndIndexOfCondWithAlternative(stmt, processedIfs) + } + (startIndex, endIndex, csType) + } + + /** + * This function determines the indices of the very first and very last statement that belong to + * the `switch` statement as well as the type of the `switch` ( + * [[NestedPathType.CondWithAlternative]] if the `switch` has a `default` case and + * [[NestedPathType.CondWithoutAlternative]] otherwise. + * + * @param stmt The index of the statement to process. This statement must be of type [[Switch]]. + * + * @return Returns the start index, end index, and type of the `switch` in that order. + */ + protected def processSwitch(stmt: Int): CSInfo = { + val switch = cfg.code.instructions(stmt).asSwitch + val caseStmts = switch.caseStmts.sorted + // From the last to the first one, find the first case that points after the switch + val caseGotoOption = caseStmts.reverse.find { caseIndex ⇒ + cfg.code.instructions(caseIndex - 1).isInstanceOf[Goto] + } + // If no such case is present, find the next goto after the default case + val posGoTo = if (caseGotoOption.isEmpty) { + var i = switch.defaultStmt + while (!cfg.code.instructions(i).isInstanceOf[Goto]) { + i += 1 + } + i + } else caseGotoOption.get - 1 + var end = cfg.code.instructions(posGoTo).asGoto.targetStmt - 1 + // In case the goto points at the a loop, do not set the start index of the loop as end + // position but the index of the goto + if (end < stmt) { + end = posGoTo + } + + val containsDefault = caseStmts.length == caseStmts.distinct.length + val pathType = if (containsDefault) NestedPathType.CondWithAlternative else + NestedPathType.CondWithoutAlternative + + (stmt, end, pathType) + } + + /** + * @param stmtIndex The index of the instruction that is an [[If]] and for which the type is to + * be determined. + * @return Returns a value in [[NestedPathType.Value]] except + * [[NestedPathType.TryCatchFinally]] (as their construction does not involve an [[If]] + * statement). + */ + protected def determineTypeOfIf( + stmtIndex: Int, processedIfs: mutable.Map[Int, Unit.type] + ): NestedPathType.Value = { + // Is the first condition enough to identify loops? + val loops = cfg.findNaturalLoops() + // The if might belong to the head or end of the loop + if (isHeadOfLoop(stmtIndex, loops, cfg) || isEndOfLoop(stmtIndex, loops)) { + NestedPathType.Repetition + } else if (isCondWithoutElse(stmtIndex, cfg, processedIfs)) { + NestedPathType.CondWithoutAlternative + } else { + NestedPathType.CondWithAlternative + } + } + + /** + * Finds all control structures within [[cfg]]. This includes `try-catch`. + * `try-catch` blocks will be treated specially in the sense that, if a ''finally'' block + * exists, it will not be included in the path from ''start index'' to ''destination index'' + * (however, as ''start index'' marks the beginning of the `try-catch` and ''destination index'' + * everything up to the ''finally block'', ''finally'' statements after the exception handling + * will be included and need to be filtered out later. + * + * @return Returns all found control structures in a flat structure; for the return format, see + * [[CSInfo]]. The elements are returned in a sorted by ascending start index. + */ + protected def findControlStructures(startSites: List[Int], endSite: Int): List[CSInfo] = { + // foundCS stores all found control structures as a triple in the form (start, end, type) + var foundCS = ListBuffer[CSInfo]() + // For a fast loop-up which if statements have already been processed + val processedIfs = mutable.Map[Int, Unit.type]() + val processedSwitches = mutable.Map[Int, Unit.type]() + val stack = mutable.Stack[CFGNode]() + val seenCFGNodes = mutable.Map[CFGNode, Unit.type]() + + startSites.reverse.foreach { site ⇒ + stack.push(cfg.bb(site)) + seenCFGNodes(cfg.bb(site)) = Unit + } + + while (stack.nonEmpty) { + val next = stack.pop() + seenCFGNodes(next) = Unit + + next match { + case bb: BasicBlock ⇒ + for (i ← bb.startPC.to(bb.endPC)) { + cfg.code.instructions(i) match { + case _: If[V] if !processedIfs.contains(i) ⇒ + foundCS.append(processIf(i, processedIfs)) + processedIfs(i) = Unit + case _: Switch[V] if !processedSwitches.contains(i) ⇒ + foundCS.append(processSwitch(i)) + processedSwitches(i) = Unit + case _ ⇒ + } + } + case _ ⇒ + } + + if (next.nodeId == endSite) { + val doesPathExist = stack.filter(_.nodeId >= 0).foldLeft(false) { + (doesExist: Boolean, next: CFGNode) ⇒ + doesExist || doesPathExistTo(next.nodeId, endSite) + } + // In case no more path exists, clear the stack which (=> no more iterations) + if (!doesPathExist) { + stack.clear() + } + } else { + // Add unseen successors + next.successors.filter(!seenCFGNodes.contains(_)).foreach(stack.push) + } + } + + // It might be that some control structures can be removed as they are not in the relevant + // range + foundCS = foundCS.filterNot { + case (start, end, _) ⇒ + (startSites.forall(start > _) && endSite < start) || + (startSites.forall(_ < start) && startSites.forall(_ > end)) + } + + // Add try-catch (only those that are relevant for the given start and end sites) + // information + var relevantTryCatchBlocks = determineTryCatchBounds() + // Filter out all blocks that completely surround the given start and end sites + relevantTryCatchBlocks = relevantTryCatchBlocks.filter { + case (tryStart, tryEnd, _) ⇒ + val tryCatchParts = buildTryCatchPath(tryStart, tryEnd, fill = false) + !tryCatchParts._2.exists { + case (nextInnerStart, nextInnerEnd) ⇒ + startSites.forall(_ >= nextInnerStart) && endSite <= nextInnerEnd + } + } + // Keep the try-catch blocks that are (partially) within the start and end sites + relevantTryCatchBlocks = relevantTryCatchBlocks.filter { + case (tryStart, _, _) ⇒ + startSites.exists(tryStart >= _) && tryStart <= endSite + } + + foundCS.appendAll(relevantTryCatchBlocks) + foundCS.sortBy { case (start, _, _) ⇒ start }.toList + } + + /** + * This function serves as a wrapper function for unified processing of different elements, + * i.e., different types of [[CSInfo]] that are stored in `toTransform`. + * For further information, see [[buildRepetitionPath]], [[buildCondPath]], + * [[buildPathForSwitch]], and [[buildTryCatchPath]]. + */ + protected def buildPathForElement( + toTransform: HierarchicalCSOrder, fill: Boolean + ): (Path, List[(Int, Int)]) = { + val element = toTransform.hierarchy.head._1.get + val start = element._1 + val end = element._2 + if (cfg.code.instructions(start).isInstanceOf[Switch[V]]) { + buildPathForSwitch(start, end, fill) + } else { + element._3 match { + case NestedPathType.Repetition ⇒ + buildRepetitionPath(start, end, fill) + case NestedPathType.CondWithAlternative ⇒ + buildCondPath(start, end, NestedPathType.CondWithAlternative, fill) + case NestedPathType.CondWithoutAlternative ⇒ + buildCondPath(start, end, NestedPathType.CondWithoutAlternative, fill) + case NestedPathType.TryCatchFinally ⇒ + buildTryCatchPath(start, end, fill) + } + } + } + + /** + * This function takes a flat list of control structure information and transforms it into a + * hierarchical order. + * + * @param cs A list of control structure elements that are to be transformed into a hierarchical + * representation. This function assumes, that the control structures are sorted by + * start index in ascending order. + * @return The hierarchical structure. + * + * @note This function assumes that `cs` contains at least one element! + */ + protected def hierarchicallyOrderControlStructures(cs: List[CSInfo]): HierarchicalCSOrder = { + // childrenOf stores seen control structures in the form: parent, children. Note that for + // performance reasons (see foreach loop below), the elements are inserted in reversed order + // in terms of the `cs` order for less loop iterations in the next foreach loop + val childrenOf = mutable.ListBuffer[(CSInfo, ListBuffer[CSInfo])]() + childrenOf.append((cs.head, ListBuffer())) + + // Stores as key a CS and as value the parent element (if an element, e, is not contained in + // parentOf, e does not have a parent + val parentOf = mutable.Map[CSInfo, CSInfo]() + // Find the direct parent of each element (if it exists at all) + cs.tail.foreach { nextCS ⇒ + var nextPossibleParentIndex = 0 + var parent: Option[Int] = None + // Use a while instead of a foreach loop in order to stop when the parent was found + while (parent.isEmpty && nextPossibleParentIndex < childrenOf.length) { + val possibleParent = childrenOf(nextPossibleParentIndex) + // The parent element must contain the child + if (nextCS._1 > possibleParent._1._1 && nextCS._1 <= possibleParent._1._2) { + parent = Some(nextPossibleParentIndex) + } else { + nextPossibleParentIndex += 1 + } + } + if (parent.isDefined) { + childrenOf(parent.get)._2.append(nextCS) + parentOf(nextCS) = childrenOf(parent.get)._1 + } + childrenOf.prepend((nextCS, ListBuffer())) + } + + // Convert to a map for faster accesses in the following part + val mapChildrenOf = mutable.Map[CSInfo, ListBuffer[CSInfo]]() + childrenOf.foreach { nextCS ⇒ mapChildrenOf(nextCS._1) = nextCS._2 } + + HierarchicalCSOrder(List(( + None, cs.filter(!parentOf.contains(_)).map(buildHierarchy(_, mapChildrenOf)) + ))) + } + + /** + * This function transforms a hierarchy into a [[Path]]. + * + * @param topElements A list of the elements which are present on the top-most level in the + * hierarchy. + * @param startIndex `startIndex` serves as a way to build a path between the first statement + * (which is not necessarily a control structure) and the very first control + * structure. For example, assume that the first control structure begins at + * statement 5. `startIndex` will then be used to fill the gap `startIndex` + * and 5. + * @param endIndex `endIndex` serves as a way to build a path between the last statement of a + * control structure (which is not necessarily the end of a scope of interest, + * such as a method) and the last statement (e.g., in `cfg`). + * @return Returns the transformed [[Path]]. + */ + protected def hierarchyToPath( + topElements: List[HierarchicalCSOrder], startIndex: Int, endIndex: Int + ): Path = { + val finalPath = ListBuffer[SubPath]() + // For the outer-most call, this is not the start index of the last control structure but of + // the start PC of the first basic block + var indexLastCSEnd = startIndex + + // Recursively transform the hierarchies to paths + topElements.foreach { nextTopEle ⇒ + // Build path up to the next control structure + val nextCSStart = nextTopEle.hierarchy.head._1.get._1 + indexLastCSEnd.until(nextCSStart).foreach { i ⇒ + finalPath.append(FlatPathElement(i)) + } + + val children = nextTopEle.hierarchy.head._2 + if (children.isEmpty) { + // Recursion anchor: Build path for the correct type + val (subpath, _) = buildPathForElement(nextTopEle, fill = true) + // Control structures consist of only one element (NestedPathElement), thus "head" + // is enough + finalPath.append(subpath.elements.head) + } else { + val startIndex = nextTopEle.hierarchy.head._1.get._1 + val endIndex = nextTopEle.hierarchy.head._1.get._2 + val childrenPath = hierarchyToPath(children, startIndex, endIndex) + var insertIndex = 0 + val (subpath, startEndPairs) = buildPathForElement(nextTopEle, fill = false) + // npe is the nested path element that was produced above (head is enough as this + // list will always contain only one element, due to fill=false) + val npe = subpath.elements.head.asInstanceOf[NestedPathElement] + val isRepElement = npe.elementType.getOrElse(NestedPathType.TryCatchFinally) == + NestedPathType.Repetition + var lastInsertedIndex = 0 + childrenPath.elements.foreach { nextEle ⇒ + if (isRepElement) { + npe.element.append(nextEle) + } else { + if (insertIndex < npe.element.length) { + npe.element(insertIndex).asInstanceOf[NestedPathElement].element.append( + nextEle + ) + } + } + + lastInsertedIndex = nextEle match { + case fpe: FlatPathElement ⇒ fpe.element + case inner: NestedPathElement ⇒ Path.getLastElementInNPE(inner).element + // Compiler wants it but should never be the case! + case _ ⇒ -1 + } + if (insertIndex < startEndPairs.length && + lastInsertedIndex >= startEndPairs(insertIndex)._2) { + insertIndex += 1 + } + } + // Fill the current NPE if necessary + val currentToInsert = ListBuffer[FlatPathElement]() + if (insertIndex < startEndPairs.length) { + currentToInsert.appendAll((lastInsertedIndex + 1).to( + startEndPairs(insertIndex)._2 + ).map(FlatPathElement)) + if (isRepElement) { + npe.element.appendAll(currentToInsert) + } else { + var insertPos = npe.element(insertIndex).asInstanceOf[NestedPathElement] + insertPos.element.appendAll(currentToInsert) + insertIndex += 1 + // Fill the rest NPEs if necessary + insertIndex.until(startEndPairs.length).foreach { i ⇒ + insertPos = npe.element(i).asInstanceOf[NestedPathElement] + insertPos.element.appendAll( + startEndPairs(i)._1.to(startEndPairs(i)._2).map(FlatPathElement) + ) + } + } + } + // Make sure to have no empty lists + val subPathNpe = subpath.elements.head.asInstanceOf[NestedPathElement] + val subPathToAdd = NestedPathElement( + subPathNpe.element.filter { + case npe: NestedPathElement ⇒ npe.element.nonEmpty + case _ ⇒ true + }, subPathNpe.elementType + ) + finalPath.append(subPathToAdd) + } + indexLastCSEnd = nextTopEle.hierarchy.head._1.get._2 + 1 + } + + finalPath.appendAll(indexLastCSEnd.to(endIndex).map(FlatPathElement)) + Path(finalPath.toList) + } + + /** + * Implementations of this function find all paths starting from the sites, given by + * `startSites`, within the provided control flow graph, `cfg`. As this is executed within the + * context of a string definition analysis, implementations are free to decide whether they + * include only statements that work on [[StringBuffer]] / [[StringBuilder]] or include all + * statements in the paths. + * + * @param startSites A list of possible start sites, that is, initializations. Several start + * sites denote that an object is initialized within a conditional. + * Implementations may or may not use this list (however, they should indicate + * whether it is required or not). + * @param endSite An end site, that is, if the element corresponding to `endSite` is + * encountered, the finding procedure can be early stopped. Implementations + * may or may not use this list (however, they should indicate whether it is + * required or not). + * @return Returns all found paths as a [[Path]] object. That means, the return object is a flat + * structure, however, captures all hierarchies and (nested) flows. Note that a + * [[NestedPathElement]] with only one child can either refer to a loop or an ''if'' + * that has no ''else'' block (from a high-level perspective). It is the job of the + * implementations to attach these information to [[NestedPathElement]]s (so that + * procedures using results of this function do not need to re-process). + */ + def findPaths(startSites: List[Int], endSite: Int): Path + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/DefaultPathFinder.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/DefaultPathFinder.scala new file mode 100644 index 0000000000..90d7738048 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/DefaultPathFinder.scala @@ -0,0 +1,51 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.preprocessing + +import org.opalj.br.cfg.CFG +import org.opalj.tac.Stmt +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V + +/** + * An approach based on an intuitive traversing of the control flow graph (CFG). This implementation + * will use the CFG to find all paths from the very first statement of the CFG to all end / leaf + * statements in the CFG, ignoring `startSites` and `endSite` passed to + * [[DefaultPathFinder#findPaths]]. + * + * @param cfg The CFG on which this instance will operate on. + * + * @author Patrick Mell + * + * @note To fill gaps, e.g., from the very first statement of a context, such as a CFG, to the first + * control structure, a consecutive row of path elements are inserted. Arbitrarily inserted + * jumps within the bytecode might lead to a different order than the one computed by this + * class! + */ +class DefaultPathFinder(cfg: CFG[Stmt[V], TACStmts[V]]) extends AbstractPathFinder(cfg) { + + /** + * This implementation finds all paths based on an a naive / intuitive traversing of the `cfg` + * and, based on that, determines in what relation a statement / instruction is with its + * predecessors / successors. + * The paths contain all instructions, not only those that modify a [[StringBuilder]] / + * [[StringBuffer]] object. + * In this implementation, `startSites` as well as `endSite` are ignored, i.e., it is fine to + * pass any values for these two. + * + * @see [[AbstractPathFinder.findPaths]] + */ + override def findPaths(startSites: List[Int], endSite: Int): Path = { + val startSite = cfg.startBlock.startPC + val endSite = cfg.code.instructions.length - 1 + val csInfo = findControlStructures(List(startSite), endSite) + // In case the are no control structures, return a path from the first to the last element + if (csInfo.isEmpty) { + Path(cfg.startBlock.startPC.until(endSite).map(FlatPathElement).toList) + } // Otherwise, order the control structures and assign the corresponding path elements + else { + val orderedCS = hierarchicallyOrderControlStructures(csInfo) + hierarchyToPath(orderedCS.hierarchy.head._2, startSite, endSite) + } + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/Path.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/Path.scala new file mode 100644 index 0000000000..45d98401b8 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/Path.scala @@ -0,0 +1,350 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.preprocessing + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.value.ValueInformation +import org.opalj.tac.Assignment +import org.opalj.tac.DUVar +import org.opalj.tac.ExprStmt +import org.opalj.tac.New +import org.opalj.tac.Stmt +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.fpcf.analyses.string_analysis.V +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * @author Patrick Mell + */ + +/** + * [[SubPath]] represents the general item that forms a [[Path]]. + */ +sealed class SubPath() + +/** + * A flat element, e.g., for representing a single statement. The statement is identified by + * `element`. + */ +case class FlatPathElement(element: Int) extends SubPath + +/** + * Identifies the nature of a nested path element. + */ +object NestedPathType extends Enumeration { + + /** + * Used to mark any sort of loops. + */ + val Repetition: NestedPathType.Value = Value + + /** + * Use this type to mark a conditional that has an alternative that is guaranteed to be + * executed. For instance, an `if` with an `else` block would fit this type, as would a `case` + * with a `default`. These are just examples for high-level languages. The concepts, however, + * can be applied to low-level format as well. + */ + val CondWithAlternative: NestedPathType.Value = Value + + /** + * Use this type to mark a conditional that is not necessarily executed. For instance, an `if` + * without an `else` (but possibly several `else if` fits this category. Again, this is to be + * mapped to low-level representations as well. + */ + val CondWithoutAlternative: NestedPathType.Value = Value + + /** + * This type is to mark `try-catch` or `try-catch-finally` constructs. + */ + val TryCatchFinally: NestedPathType.Value = Value + +} + +/** + * A nested path element, that is, items can be used to form arbitrary structures / hierarchies. + * `element` holds all child elements. Path finders should set the `elementType` property whenever + * possible, i.e., when they compute / have this information. + */ +case class NestedPathElement( + element: ListBuffer[SubPath], + elementType: Option[NestedPathType.Value] +) extends SubPath + +/** + * Models a path by assembling it out of [[SubPath]] elements. + * + * @param elements The elements that belong to a path. + */ +case class Path(elements: List[SubPath]) { + + /** + * Takes an object of interest, `obj`, and a list of statements, `stmts` and finds all + * definitions and usages of `obj`within `stmts`. These sites are then returned in a single + * sorted list. + */ + private def getAllDefAndUseSites( + obj: DUVar[ValueInformation], stmts: Array[Stmt[V]] + ): List[Int] = { + val defAndUses = ListBuffer[Int]() + val stack = mutable.Stack[Int](obj.definedBy.toArray: _*) + + while (stack.nonEmpty) { + val popped = stack.pop() + if (!defAndUses.contains(popped)) { + defAndUses.append(popped) + + stmts(popped) match { + case a: Assignment[V] if a.expr.isInstanceOf[VirtualFunctionCall[V]] ⇒ + val receiver = a.expr.asVirtualFunctionCall.receiver.asVar + stack.pushAll(receiver.asVar.definedBy.filter(_ >= 0).toArray) + // TODO: Does the following line add too much (in some cases)??? + stack.pushAll(a.targetVar.asVar.usedBy.toArray) + case a: Assignment[V] if a.expr.isInstanceOf[New] ⇒ + stack.pushAll(a.targetVar.usedBy.toArray) + case _ ⇒ + } + } + } + + defAndUses.toList.sorted + } + + /** + * Takes a `subpath` and checks whether the given `element` is contained. This function does a + * deep search, i.e., will also find the element if it is contained within + * [[NestedPathElement]]s. + */ + private def containsPathElement(subpath: NestedPathElement, element: Int): Boolean = { + subpath.element.foldLeft(false) { (old: Boolean, nextSubpath: SubPath) ⇒ + old || (nextSubpath match { + case fpe: FlatPathElement ⇒ fpe.element == element + case npe: NestedPathElement ⇒ containsPathElement(npe, element) + // For the SubPath type (should never be the case, but the compiler wants it) + case _ ⇒ false + }) + } + } + + /** + * Takes a [[NestedPathElement]] and removes the outermost nesting, i.e., the path contained + * in `npe` will be the path being returned. + */ + private def removeOuterBranching(npe: NestedPathElement): ListBuffer[SubPath] = { + if (npe.element.tail.isEmpty) { + npe.element.head match { + case innerNpe: NestedPathElement ⇒ removeOuterBranching(innerNpe) + case fpe: SubPath ⇒ ListBuffer[SubPath](fpe) + } + } else { + ListBuffer[SubPath](npe.element: _*) + } + } + + /** + * Takes a [[NestedPathElement]], `npe`, and an `endSite` and strips all branches that do not + * contain `endSite`. ''Stripping'' here means to clear the other branches. + * For example, assume `npe=[[3, 5], [7, 9]]` and `endSite=7`, the this function will return + * `[[], [7, 9]]`. This function can handle deeply nested [[NestedPathElement]] expressions as + * well. + */ + private def stripUnnecessaryBranches( + npe: NestedPathElement, endSite: Int + ): NestedPathElement = { + npe.element.foreach { + case innerNpe: NestedPathElement ⇒ + if (innerNpe.elementType.isEmpty) { + if (!containsPathElement(innerNpe, endSite)) { + innerNpe.element.clear() + } + } else { + stripUnnecessaryBranches(innerNpe, endSite) + } + case _ ⇒ + } + npe + } + + /** + * Accumulator function for transforming a path into its lean equivalent. This function turns + * [[NestedPathElement]]s into lean [[NestedPathElement]]s and is a helper function of + * [[makeLeanPath]]. + * + * @param toProcess The NestedPathElement to turn into its lean equivalent. + * @param siteMap Serves as a look-up table to include only elements that are of interest, in + * this case: That belong to some object. + * @param endSite `endSite` is an denotes an element which is sort of a border between elements + * to include into the lean path and which not to include. For example, if a read + * operation, which is of interest, occurs not at the end of the given `toProcess` + * path, the rest can be safely omitted (as the paths already are in a + * happens-before relationship). If all elements are included, pass an int value + * that is greater than the greatest index of the elements in `toProcess`. + * @param includeAlternatives For cases where an operation of interest happens within a branch + * of an `if-else` constructions , it is not necessary to include the + * other branches (as they are mutually exclusive anyway). + * `includeAlternatives = false` represents this behavior. However, + * sometimes it is desired to include all alternatives as in the case + * of `try-catch(-finally)` constructions). + * @return In case a (sub) path is empty, `None` is returned and otherwise the lean (sub) path. + */ + private def makeLeanPathAcc( + toProcess: NestedPathElement, + siteMap: Map[Int, Unit.type], + endSite: Int, + includeAlternatives: Boolean = false + ): (Option[NestedPathElement], Boolean) = { + val elements = ListBuffer[SubPath]() + var stop = false + var hasTargetBeenSeen = false + val isTryCatch = includeAlternatives || (toProcess.elementType.isDefined && + toProcess.elementType.get == NestedPathType.TryCatchFinally) + + toProcess.element.foreach { next ⇒ + // The stop flag is used to make sure that within a sub-path only the elements up to the + // endSite are gathered (if endSite is within this sub-path) + if (!stop) { + next match { + case fpe: FlatPathElement if !hasTargetBeenSeen ⇒ + if (siteMap.contains(fpe.element) && !hasTargetBeenSeen) { + elements.append(fpe.copy()) + } + if (fpe.element == endSite) { + hasTargetBeenSeen = true + stop = true + } + case npe: NestedPathElement if isTryCatch ⇒ + val (leanedSubPath, _) = makeLeanPathAcc( + npe, siteMap, endSite, includeAlternatives = true + ) + if (leanedSubPath.isDefined) { + elements.append(leanedSubPath.get) + } + case npe: NestedPathElement ⇒ + if (!hasTargetBeenSeen) { + val (leanedSubPath, wasTargetSeen) = makeLeanPathAcc( + npe, siteMap, endSite + ) + if (leanedSubPath.isDefined) { + elements.append(leanedSubPath.get) + } + if (wasTargetSeen) { + hasTargetBeenSeen = true + } + } + case _ ⇒ + } + } + } + + if (elements.nonEmpty) { + (Some(NestedPathElement(elements, toProcess.elementType)), hasTargetBeenSeen) + } else { + (None, false) + } + } + + /** + * Takes `this` path and transforms it into a new [[Path]] where only those sites are contained + * that either use or define `obj`. + * + * @param obj Identifies the object of interest. That is, all definition and use sites of this + * object will be kept in the resulting lean path. `obj` should refer to a use site, + * most likely corresponding to an (implicit) `toString` call. + * @param stmts A list of look-up statements, i.e., a program / method description in which + * `obj` occurs. + * @return Returns a lean path of `this` path. That means, `this` instance will be stripped to + * contain only [[FlatPathElement]]s and [[NestedPathElement]]s that contain a + * definition or usage of `obj`. This includes the removal of [[NestedPathElement]]s + * not containing `obj`. + * + * @note This function does not change the underlying `this` instance. Furthermore, all relevant + * elements for the lean path will be copied, i.e., `this` instance and the returned + * instance do not share any references. + */ + def makeLeanPath(obj: DUVar[ValueInformation], stmts: Array[Stmt[V]]): Path = { + val newOfObj = InterpretationHandler.findNewOfVar(obj, stmts) + // Transform the list of relevant sites into a map to have a constant access time + val siteMap = getAllDefAndUseSites(obj, stmts).filter { nextSite ⇒ + stmts(nextSite) match { + case Assignment(_, _, expr: VirtualFunctionCall[V]) ⇒ + val news = InterpretationHandler.findNewOfVar(expr.receiver.asVar, stmts) + newOfObj == news || news.exists(newOfObj.contains) + case ExprStmt(_, expr: VirtualFunctionCall[V]) ⇒ + val news = InterpretationHandler.findNewOfVar(expr.receiver.asVar, stmts) + newOfObj == news || news.exists(newOfObj.contains) + case _ ⇒ true + } + }.map { s ⇒ (s, Unit) }.toMap + var leanPath = ListBuffer[SubPath]() + val endSite = obj.definedBy.toArray.max + var reachedEndSite = false + + elements.foreach { next ⇒ + if (!reachedEndSite) { + next match { + case fpe: FlatPathElement if siteMap.contains(fpe.element) ⇒ + leanPath.append(fpe) + if (fpe.element == endSite) { + reachedEndSite = true + } + case npe: NestedPathElement ⇒ + val (leanedPath, wasTargetSeen) = makeLeanPathAcc(npe, siteMap, endSite) + if (npe.elementType.isDefined && + npe.elementType.get != NestedPathType.TryCatchFinally) { + reachedEndSite = wasTargetSeen + } + if (leanedPath.isDefined) { + leanPath.append(leanedPath.get) + } + case _ ⇒ + } + } + } + + // If everything is within a single branch of a nested path element, ignore it (it is not + // relevant, as everything happens within that branch anyway); for loops, remove the outer + // body in any case (as there is no alternative branch to consider) + if (leanPath.tail.isEmpty) { + leanPath.head match { + case npe: NestedPathElement if npe.elementType.get == NestedPathType.Repetition || + npe.element.tail.isEmpty ⇒ + leanPath = removeOuterBranching(npe) + case _ ⇒ + } + } else { + // If the last element is a conditional, keep only the relevant branch (the other is not + // necessary and stripping it simplifies further steps; explicitly exclude try-catch) + leanPath.last match { + case npe: NestedPathElement if npe.elementType.isDefined && + (npe.elementType.get != NestedPathType.TryCatchFinally) ⇒ + val newLast = stripUnnecessaryBranches(npe, endSite) + leanPath.remove(leanPath.size - 1) + leanPath.append(newLast) + case _ ⇒ + } + } + + Path(leanPath.toList) + } + +} + +object Path { + + /** + * Returns the very last [[FlatPathElement]] in this path, respecting any nesting structure. + */ + def getLastElementInNPE(npe: NestedPathElement): FlatPathElement = { + npe.element.last match { + case fpe: FlatPathElement ⇒ fpe + case npe: NestedPathElement ⇒ + npe.element.last match { + case fpe: FlatPathElement ⇒ fpe + case innerNpe: NestedPathElement ⇒ getLastElementInNPE(innerNpe) + case _ ⇒ FlatPathElement(-1) + } + case _ ⇒ FlatPathElement(-1) + } + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/PathTransformer.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/PathTransformer.scala new file mode 100644 index 0000000000..ddddbe3be0 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/PathTransformer.scala @@ -0,0 +1,172 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.preprocessing + +import scala.collection.mutable.ListBuffer +import scala.collection.mutable.Map + +import org.opalj.br.fpcf.properties.properties.StringTree +import org.opalj.br.fpcf.properties.string_definition.StringConstancyInformation +import org.opalj.br.fpcf.properties.string_definition.StringTreeConcat +import org.opalj.br.fpcf.properties.string_definition.StringTreeCond +import org.opalj.br.fpcf.properties.string_definition.StringTreeConst +import org.opalj.br.fpcf.properties.string_definition.StringTreeOr +import org.opalj.br.fpcf.properties.string_definition.StringTreeRepetition +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.fpcf.analyses.string_analysis.interpretation.InterpretationHandler + +/** + * [[PathTransformer]] is responsible for transforming a [[Path]] into another representation, such + * as [[org.opalj.br.fpcf.properties.properties.StringTree]]s for example. + * An instance can handle several consecutive transformations of different paths as long as they + * refer to the underlying control flow graph. If this is no longer the case, create a new instance + * of this class with the corresponding (new) `cfg?`. + * + * @param interpretationHandler An concrete instance of [[InterpretationHandler]] that is used to + * process expressions / definition sites. + * + * @author Patrick Mell + */ +class PathTransformer(val interpretationHandler: InterpretationHandler) { + + /** + * Accumulator function for transforming a path into a StringTree element. + */ + private def pathToTreeAcc( + subpath: SubPath, fpe2Sci: Map[Int, ListBuffer[StringConstancyInformation]] + ): Option[StringTree] = { + subpath match { + case fpe: FlatPathElement ⇒ + val sci = if (fpe2Sci.contains(fpe.element)) { + StringConstancyInformation.reduceMultiple(fpe2Sci(fpe.element)) + } else { + val r = interpretationHandler.processDefSite(fpe.element) + val sciToAdd = if (r.isFinal) { + r.asFinal.p.asInstanceOf[StringConstancyProperty].stringConstancyInformation + } else { + // processDefSite is not guaranteed to return a StringConstancyProperty => + // fall back to lower bound is necessary + if (r.isEPK || r.isEPS) { + StringConstancyInformation.lb + } else { + r.asInterim.ub match { + case property: StringConstancyProperty ⇒ + property.stringConstancyInformation + case _ ⇒ + StringConstancyInformation.lb + } + } + } + fpe2Sci(fpe.element) = ListBuffer(sciToAdd) + sciToAdd + } + if (sci.isTheNeutralElement) { + None + } else { + Some(StringTreeConst(sci)) + } + case npe: NestedPathElement ⇒ + if (npe.elementType.isDefined) { + npe.elementType.get match { + case NestedPathType.Repetition ⇒ + val processedSubPath = pathToStringTree( + Path(npe.element.toList), fpe2Sci, resetExprHandler = false + ) + Some(StringTreeRepetition(processedSubPath)) + case _ ⇒ + val processedSubPaths = npe.element.map { ne ⇒ + pathToTreeAcc(ne, fpe2Sci) + }.filter(_.isDefined).map(_.get) + if (processedSubPaths.nonEmpty) { + npe.elementType.get match { + case NestedPathType.CondWithAlternative | + NestedPathType.TryCatchFinally ⇒ + // In case there is only one element in the sub path, + // transform it into a conditional element (as there is no + // alternative) + if (processedSubPaths.tail.nonEmpty) { + Some(StringTreeOr(processedSubPaths)) + } else { + Some(StringTreeCond(processedSubPaths)) + } + case NestedPathType.CondWithoutAlternative ⇒ + Some(StringTreeCond(processedSubPaths)) + case _ ⇒ None + } + } else { + None + } + } + } else { + npe.element.size match { + case 0 ⇒ None + case 1 ⇒ pathToTreeAcc(npe.element.head, fpe2Sci) + case _ ⇒ + val processed = npe.element.map { ne ⇒ + pathToTreeAcc(ne, fpe2Sci) + }.filter(_.isDefined).map(_.get) + if (processed.isEmpty) { + None + } else { + Some(StringTreeConcat(processed)) + } + } + } + case _ ⇒ None + } + } + + /** + * Takes a [[Path]] and transforms it into a [[StringTree]]. This implies an interpretation of + * how to handle methods called on the object of interest (like `append`). + * + * @param path The path element to be transformed. + * @param fpe2Sci A mapping from [[FlatPathElement.element]] values to + * [[StringConstancyInformation]]. Make use of this mapping if some + * StringConstancyInformation need to be used that the + * [[org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural.IntraproceduralInterpretationHandler]] + * cannot infer / derive. For instance, if the exact value of an + * expression needs to be determined by calling the + * [[org.opalj.tac.fpcf.analyses.string_analysis.IntraproceduralStringAnalysis]] + * on another instance, store this information in fpe2Sci. + * @param resetExprHandler Whether to reset the underlying + * [[org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural.IntraproceduralInterpretationHandler]] + * or not. When calling this function from outside, the default value + * should do fine in most of the cases. For further information, see + * [[org.opalj.tac.fpcf.analyses.string_analysis.interpretation.intraprocedural.IntraproceduralInterpretationHandler.reset]]. + * + * @return If an empty [[Path]] is given, `None` will be returned. Otherwise, the transformed + * [[org.opalj.br.fpcf.properties.properties.StringTree]] will be returned. Note that + * all elements of the tree will be defined, i.e., if `path` contains sites that could + * not be processed (successfully), they will not occur in the tree. + */ + def pathToStringTree( + path: Path, + fpe2Sci: Map[Int, ListBuffer[StringConstancyInformation]] = Map.empty, + resetExprHandler: Boolean = true + ): StringTree = { + val tree = path.elements.size match { + case 1 ⇒ + // It might be that for some expressions, a neutral element is produced which is + // filtered out by pathToTreeAcc; return the lower bound in such cases + pathToTreeAcc(path.elements.head, fpe2Sci).getOrElse( + StringTreeConst(StringConstancyProperty.lb.stringConstancyInformation) + ) + case _ ⇒ + val concatElement = StringTreeConcat(path.elements.map { ne ⇒ + pathToTreeAcc(ne, fpe2Sci) + }.filter(_.isDefined).map(_.get).to[ListBuffer]) + // It might be that concat has only one child (because some interpreters might have + // returned an empty list => In case of one child, return only that one + if (concatElement.children.size == 1) { + concatElement.children.head + } else { + concatElement + } + } + if (resetExprHandler) { + interpretationHandler.reset() + } + tree + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/WindowPathFinder.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/WindowPathFinder.scala new file mode 100644 index 0000000000..a20cda7b3a --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/preprocessing/WindowPathFinder.scala @@ -0,0 +1,78 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses.string_analysis.preprocessing + +import org.opalj.br.cfg.CFG +import org.opalj.tac.If +import org.opalj.tac.Stmt +import org.opalj.tac.Switch +import org.opalj.tac.TACStmts +import org.opalj.tac.fpcf.analyses.string_analysis.V + +/** + * An approach based on an intuitive traversing of the control flow graph (CFG). This implementation + * will use the CFG to find all paths from the given `startSites` to the `endSite`. ("Window" as + * only part of the whole CFG is considered.) + * + * @param cfg The CFG on which this instance will operate on. + * + * @author Patrick Mell + * + * @note To fill gaps, e.g., from the very first statement of a context, such as a CFG, to the first + * control structure, a consecutive row of path elements are inserted. Arbitrarily inserted + * jumps within the bytecode might lead to a different order than the one computed by this + * class! + */ +class WindowPathFinder(cfg: CFG[Stmt[V], TACStmts[V]]) extends AbstractPathFinder(cfg) { + + /** + * This implementation finds all paths based on an a naive / intuitive traversing of the `cfg` + * and, based on that, determines in what relation a statement / instruction is with its + * predecessors / successors. + * The paths contain all instructions, not only those that modify a [[StringBuilder]] / + * [[StringBuffer]] object. + * For this implementation, `startSites` as well as `endSite` are required! + * + * @see [[AbstractPathFinder.findPaths]] + */ + override def findPaths(startSites: List[Int], endSite: Int): Path = { + // If there are multiple start sites, find the parent "if" or "switch" and use that as a + // start site + var startSite: Option[Int] = None + if (startSites.tail.nonEmpty) { + var nextStmt = startSites.min + while (nextStmt >= 0 && startSite.isEmpty) { + cfg.code.instructions(nextStmt) match { + case iff: If[V] if startSites.contains(iff.targetStmt) ⇒ + startSite = Some(nextStmt) + case _: Switch[V] ⇒ + val (startSwitch, endSwitch, _) = processSwitch(nextStmt) + val isParentSwitch = startSites.forall { + nextStartSite ⇒ nextStartSite >= startSwitch && nextStartSite <= endSwitch + } + if (isParentSwitch) { + startSite = Some(nextStmt) + } + case _ ⇒ + } + nextStmt -= 1 + } + if (startSite.isEmpty) { + startSite = Some(0) + } + } else { + startSite = Some(startSites.head) + } + + val csInfo = findControlStructures(List(startSite.get), endSite) + // In case the are no control structures, return a path from the first to the last element + if (csInfo.isEmpty) { + val indexLastStmt = cfg.code.instructions.length + Path(cfg.startBlock.startPC.until(indexLastStmt).map(FlatPathElement).toList) + } // Otherwise, order the control structures and assign the corresponding path elements + else { + val orderedCS = hierarchicallyOrderControlStructures(csInfo) + hierarchyToPath(orderedCS.hierarchy.head._2, startSite.get, endSite) + } + } + +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/string_analysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/string_analysis.scala new file mode 100644 index 0000000000..bd7c787454 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string_analysis/string_analysis.scala @@ -0,0 +1,51 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.tac.fpcf.analyses + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.value.ValueInformation +import org.opalj.br.Method +import org.opalj.br.fpcf.properties.StringConstancyProperty +import org.opalj.tac.DUVar +import org.opalj.tac.FunctionCall + +/** + * @author Patrick Mell + */ +package object string_analysis { + + /** + * The type of entities the [[IntraproceduralStringAnalysis]] processes. + * + * @note The analysis requires further context information, see [[P]]. + */ + type V = DUVar[ValueInformation] + + /** + * [[IntraproceduralStringAnalysis]] processes a local variable within the context of a + * particular context, i.e., the method in which it is used. + */ + type P = (V, Method) + + /** + * This type indicates how (non-final) parameters of functions are represented. The outer-most + * list holds all parameter lists a function is called with. The list in the middle holds the + * parameters of a concrete call and the inner-most list holds interpreted parameters. The + * reason for the inner-most list is that a parameter might have different definition sites; to + * capture all, the third (inner-most) list is necessary. + */ + type NonFinalFunctionArgs = ListBuffer[ListBuffer[ListBuffer[EOptionP[Entity, StringConstancyProperty]]]] + + /** + * This type serves as a lookup mechanism to find out which functions parameters map to which + * argument position. That is, the element of type [[P]] of the inner map maps from this entity + * to its position in a data structure of type [[NonFinalFunctionArgs]]. The outer map is + * necessary to uniquely identify a position as an entity might be used for different function + * calls. + */ + type NonFinalFunctionArgsPos = mutable.Map[FunctionCall[V], mutable.Map[P, (Int, Int, Int)]] + +}