diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/StringAnalysisReflectiveCalls.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/StringAnalysisReflectiveCalls.scala new file mode 100644 index 0000000000..4121a0d735 --- /dev/null +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/StringAnalysisReflectiveCalls.scala @@ -0,0 +1,338 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package support +package info + +import scala.annotation.switch + +import java.net.URL +import scala.collection.mutable.ListBuffer +import scala.util.Try + +import org.opalj.br.DeclaredMethod +import org.opalj.br.ObjectType +import org.opalj.br.ReferenceType +import org.opalj.br.analyses.BasicReport +import org.opalj.br.analyses.DeclaredMethods +import org.opalj.br.analyses.DeclaredMethodsKey +import org.opalj.br.analyses.Project +import org.opalj.br.analyses.ProjectAnalysisApplication +import org.opalj.br.analyses.ReportableAnalysisResult +import org.opalj.br.fpcf.ContextProviderKey +import org.opalj.br.fpcf.FPCFAnalysesManagerKey +import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler +import org.opalj.br.fpcf.PropertyStoreKey +import org.opalj.br.fpcf.analyses.ContextProvider +import org.opalj.br.fpcf.properties.string.StringConstancyLevel +import org.opalj.br.fpcf.properties.string.StringConstancyProperty +import org.opalj.br.instructions.Instruction +import org.opalj.br.instructions.INVOKESTATIC +import org.opalj.br.instructions.INVOKEVIRTUAL +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.PropertyStore +import org.opalj.tac.Assignment +import org.opalj.tac.Call +import org.opalj.tac.ComputeTACAIKey +import org.opalj.tac.ExprStmt +import org.opalj.tac.StaticFunctionCall +import org.opalj.tac.Stmt +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.V +import org.opalj.tac.VirtualFunctionCall +import org.opalj.tac.cg.RTACallGraphKey +import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis +import org.opalj.tac.fpcf.analyses.fieldaccess.reflection.ReflectionRelatedFieldAccessesAnalysisScheduler +import org.opalj.tac.fpcf.analyses.string.LazyStringAnalysis +import org.opalj.tac.fpcf.analyses.string.VariableContext +import org.opalj.tac.fpcf.analyses.string.flowanalysis.LazyMethodStringFlowAnalysis +import org.opalj.tac.fpcf.analyses.string.l0.LazyL0StringFlowAnalysis +import org.opalj.tac.fpcf.analyses.string.l1.LazyL1StringFlowAnalysis +import org.opalj.tac.fpcf.analyses.string.l2.LazyL2StringFlowAnalysis +import org.opalj.tac.fpcf.analyses.string.l3.LazyL3StringFlowAnalysis +import org.opalj.tac.fpcf.analyses.string.trivial.LazyTrivialStringAnalysis +import org.opalj.tac.fpcf.analyses.systemproperties.TriggeredSystemPropertiesAnalysisScheduler +import org.opalj.util.PerformanceEvaluation.time + +/** + * Analyzes a project for calls provided by the Java Reflection API and tries to determine which string values are / + * could be passed to these calls. Includes an option to also analyze relevant JavaX Crypto API calls. + * + * @author Maximilian Rüsch + */ +object StringAnalysisReflectiveCalls extends ProjectAnalysisApplication { + + /** + * @param detectedValues Stores a list of pairs where the first element corresponds to the entities passed to the analysis and the second + * element corresponds to the method name in which the entity occurred, i.e. a value in [[relevantMethodNames]]. + */ + private case class State(detectedValues: ListBuffer[(VariableContext, String)]) + + private case class Configuration( + includeCrypto: Boolean, + private val analysisConfig: Configuration.AnalysisConfig + ) { + + def analyses: Seq[FPCFLazyAnalysisScheduler] = { + analysisConfig match { + case Configuration.TrivialAnalysis => Seq(LazyTrivialStringAnalysis) + case Configuration.LevelAnalysis(level) => + Seq( + LazyStringAnalysis, + LazyMethodStringFlowAnalysis, + Configuration.LevelToSchedulerMapping(level) + ) + } + } + } + + private object Configuration { + + private[Configuration] trait AnalysisConfig + private[Configuration] case object TrivialAnalysis extends AnalysisConfig + private[Configuration] case class LevelAnalysis(level: Int) extends AnalysisConfig + + final val LevelToSchedulerMapping = Map( + 0 -> LazyL0StringFlowAnalysis, + 1 -> LazyL1StringFlowAnalysis, + 2 -> LazyL2StringFlowAnalysis, + 3 -> LazyL3StringFlowAnalysis + ) + + def apply(parameters: Seq[String]): Configuration = { + val includeCrypto = parameters.contains("-includeCryptoApi") + val levelParameter = parameters.find(_.startsWith("-level=")).getOrElse("-level=trivial") + val analysisConfig = levelParameter.replace("-level=", "") match { + case "trivial" => TrivialAnalysis + case string => LevelAnalysis(string.toInt) + } + + new Configuration(includeCrypto, analysisConfig) + } + } + + private val relevantCryptoMethodNames = List( + "javax.crypto.Cipher#getInstance", + "javax.crypto.Cipher#getMaxAllowedKeyLength", + "javax.crypto.Cipher#getMaxAllowedParameterSpec", + "javax.crypto.Cipher#unwrap", + "javax.crypto.CipherSpi#engineSetMode", + "javax.crypto.CipherSpi#engineSetPadding", + "javax.crypto.CipherSpi#engineUnwrap", + "javax.crypto.EncryptedPrivateKeyInfo#getKeySpec", + "javax.crypto.ExemptionMechanism#getInstance", + "javax.crypto.KeyAgreement#getInstance", + "javax.crypto.KeyGenerator#getInstance", + "javax.crypto.Mac#getInstance", + "javax.crypto.SealedObject#getObject", + "javax.crypto.SecretKeyFactory#getInstance" + ) + + private val relevantReflectionMethodNames = List( + "java.lang.Class#forName", + "java.lang.ClassLoader#loadClass", + "java.lang.Class#getField", + "java.lang.Class#getDeclaredField", + "java.lang.Class#getMethod", + "java.lang.Class#getDeclaredMethod" + ) + + override def title: String = "String Analysis for Reflective Calls" + + override def description: String = { + "Finds calls to methods provided by the Java Reflection API and tries to resolve passed string values" + } + + override def analysisSpecificParametersDescription: String = + s""" + | [-includeCryptoApi] + | [-level=trivial|${Configuration.LevelToSchedulerMapping.keys.toSeq.sorted.mkString("|")}] + |""".stripMargin + + override def checkAnalysisSpecificParameters(parameters: Seq[String]): Iterable[String] = { + parameters.flatMap { + case "-includeCryptoApi" => None + case levelParameter if levelParameter.startsWith("-level=") => + levelParameter.replace("-level=", "") match { + case "trivial" => + None + case string + if Try(string.toInt).isSuccess + && Configuration.LevelToSchedulerMapping.keySet.contains(string.toInt) => + None + case value => + Some(s"Unknown level parameter value: $value") + } + + case param => Some(s"unknown parameter: $param") + } + } + + /** + * Retrieves all relevant method names, i.e., those methods from the Reflection API that have at least one string + * argument and shall be considered by this analysis. The string are supposed to have the format as produced + * by [[buildFQMethodName]]. If the 'crypto' parameter is set, relevant methods of the javax.crypto API are + * included, too. + */ + private def relevantMethodNames(implicit configuration: Configuration) = { + if (configuration.includeCrypto) + relevantReflectionMethodNames ++ relevantCryptoMethodNames + else + relevantReflectionMethodNames + } + + /** + * Using a `declaringClass` and a `methodName`, this function returns a formatted version of the + * fully-qualified method name, in the format [fully-qualified class name]#[method name] + * where the separator for the fq class names is a dot, e.g., "java.lang.Class#forName". + */ + @inline private final def buildFQMethodName(declaringClass: ReferenceType, methodName: String): String = + s"${declaringClass.toJava}#$methodName" + + /** + * Taking the `declaringClass` and the `methodName` into consideration, this function checks + * whether a method is relevant for this analysis. + * + * @note Internally, this method makes use of [[relevantMethodNames]]. A method can only be + * relevant if it occurs in [[relevantMethodNames]]. + */ + @inline private final def isRelevantCall(declaringClass: ReferenceType, methodName: String)( + implicit configuration: Configuration + ): Boolean = relevantMethodNames.contains(buildFQMethodName(declaringClass, methodName)) + + /** + * Helper function that checks whether an array of [[Instruction]]s contains at least one + * relevant method that is to be processed by `doAnalyze`. + */ + private def instructionsContainRelevantMethod(instructions: Array[Instruction])( + implicit configuration: Configuration + ): Boolean = { + instructions + .filter(_ != null) + .exists { + case INVOKESTATIC(declClass, _, methodName, _) if isRelevantCall(declClass, methodName) => true + case INVOKEVIRTUAL(declClass, methodName, _) if isRelevantCall(declClass, methodName) => true + case _ => false + } + } + + /** + * This function is a wrapper function for processing a method. It checks whether the given + * `method`, is relevant at all, and if so uses the given function `call` to call the + * analysis using the property store, `ps`, to finally store it in the given `resultMap`. + */ + private def processFunctionCall(pc: Int, dm: DeclaredMethod, call: Call[V])( + implicit + stmts: Array[Stmt[V]], + ps: PropertyStore, + contextProvider: ContextProvider, + state: State, + configuration: Configuration + ): Unit = { + if (isRelevantCall(call.declaringClass, call.name)) { + // Loop through all parameters and start the analysis for those that take a string + call.descriptor.parameterTypes.zipWithIndex.foreach { + case (ft, index) if ft == ObjectType.String => + val e = VariableContext( + pc, + call.params(index).asVar.toPersistentForm, + contextProvider.newContext(dm) + ) + ps.force(e, StringConstancyProperty.key) + state.detectedValues.append((e, buildFQMethodName(call.declaringClass, call.name))) + case _ => + } + } + } + + private def processStatements(tac: TACode[TACMethodParameter, V], dm: DeclaredMethod)( + implicit + contextProvider: ContextProvider, + ps: PropertyStore, + state: State, + configuration: Configuration + ): Unit = { + implicit val stmts: Array[Stmt[V]] = tac.stmts + stmts.foreach { stmt => + // Using the following switch speeds up the whole process + (stmt.astID: @switch) match { + case Assignment.ASTID => stmt match { + case Assignment(pc, _, c: StaticFunctionCall[V]) => + processFunctionCall(pc, dm, c) + case Assignment(pc, _, c: VirtualFunctionCall[V]) => + processFunctionCall(pc, dm, c) + case _ => + } + case ExprStmt.ASTID => stmt match { + case ExprStmt(pc, c: StaticFunctionCall[V]) => + processFunctionCall(pc, dm, c) + case ExprStmt(pc, c: VirtualFunctionCall[V]) => + processFunctionCall(pc, dm, c) + case _ => + } + case _ => + } + } + } + + override def doAnalyze( + project: Project[URL], + parameters: Seq[String], + isInterrupted: () => Boolean + ): ReportableAnalysisResult = { + implicit val state: State = State(detectedValues = ListBuffer.empty) + implicit val configuration: Configuration = Configuration(parameters) + + val cgKey = RTACallGraphKey + val typeIterator = cgKey.getTypeIterator(project) + project.updateProjectInformationKeyInitializationData(ContextProviderKey) { _ => typeIterator } + + val manager = project.get(FPCFAnalysesManagerKey) + val computeTac = project.get(ComputeTACAIKey) + val declaredMethods: DeclaredMethods = project.get(DeclaredMethodsKey) + implicit val propertyStore: PropertyStore = project.get(PropertyStoreKey) + implicit val contextProvider: ContextProvider = project.get(ContextProviderKey) + + time { + manager.runAll( + cgKey.allCallGraphAnalyses(project) ++ + configuration.analyses ++ + Seq( + EagerFieldAccessInformationAnalysis, + ReflectionRelatedFieldAccessesAnalysisScheduler, + TriggeredSystemPropertiesAnalysisScheduler + ), + afterPhaseScheduling = _ => { + project.allMethodsWithBody.foreach { m => + // To dramatically reduce work, quickly check if a method is relevant at all + if (instructionsContainRelevantMethod(m.body.get.instructions)) { + processStatements(computeTac(m), declaredMethods(m)) + } + } + } + ) + } { t => println(s"Elapsed Time: ${t.toMilliseconds}") } + + val resultMap = Map.from(relevantMethodNames.map((_, ListBuffer.empty[FinalEP[_, StringConstancyProperty]]))) + state.detectedValues.foreach { + case (e, callName) => + resultMap(callName).append(propertyStore(e, StringConstancyProperty.key).asFinal) + } + + val report = ListBuffer[String]("Results of the Reflection Analysis:") + for ((reflectiveCall, stringTrees) <- resultMap.toSeq.sortBy(_._1)) { + val invalidCount = stringTrees.count(_.p.tree.constancyLevel == StringConstancyLevel.Invalid) + val constantCount = stringTrees.count(_.p.tree.constancyLevel == StringConstancyLevel.Constant) + val partiallyConstantCount = + stringTrees.count(_.p.tree.constancyLevel == StringConstancyLevel.PartiallyConstant) + val dynamicCount = stringTrees.count(_.p.tree.constancyLevel == StringConstancyLevel.Dynamic) + + report.append(s"$reflectiveCall: ${stringTrees.length}x") + report.append(s" -> Invalid: ${invalidCount}x") + report.append(s" -> Constant: ${constantCount}x") + report.append(s" -> Partially Constant: ${partiallyConstantCount}x") + report.append(s" -> Dynamic: ${dynamicCount}x") + } + BasicReport(report) + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ArrayOps.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ArrayOps.java new file mode 100644 index 0000000000..9edcc09852 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ArrayOps.java @@ -0,0 +1,62 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.fixtures.string.tools.StringProvider; +import org.opalj.fpcf.properties.string.*; + +/** + * Various tests that test compatibility with array operations (mainly reads with specific array indexes). + * Currently, the string analysis does not support array operations and thus fails for every level. + * + * @see SimpleStringOps + */ +public class ArrayOps { + + private String[] monthNames = { "January", "February", "March", getApril() }; + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + @Constant(n = 0, levels = Level.TRUTH, value = "(java.lang.String|java.lang.StringBuilder|java.lang.System|java.lang.Runnable)") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }, reason = "arrays are not supported") + public void fromStringArray(int index) { + String[] classes = { + "java.lang.String", "java.lang.StringBuilder", + "java.lang.System", "java.lang.Runnable" + }; + if (index >= 0 && index < classes.length) { + analyzeString(classes[index]); + } + } + + @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, + value = "(java.lang.Object|java.lang.Runtime|java.lang.Integer)") + @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, + value = "(java.lang.Object|java.lang.Runtime|java.lang.Integer|.*)") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }, reason = "arrays are not supported") + public void arrayStaticAndVirtualFunctionCalls(int i) { + String[] classes = { + "java.lang.Object", + getRuntimeClassName(), + StringProvider.getFQClassNameWithStringBuilder("java.lang", "Integer"), + System.clearProperty("SomeClass") + }; + analyzeString(classes[i]); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(January|February|March|April)") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }, reason = "arrays are not supported") + public void getStringArrayField(int i) { + analyzeString(monthNames[i]); + } + + private String getRuntimeClassName() { + return "java.lang.Runtime"; + } + + private String getApril() { + return "April"; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Complex.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Complex.java new file mode 100644 index 0000000000..5898f48ca0 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Complex.java @@ -0,0 +1,126 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.lang.reflect.Method; +import java.util.Random; +import java.util.Scanner; + +/** + * Various tests that test certain complex string analysis scenarios which were either constructed or extracted from the + * JDK. Such tests should combine multiple string analysis techniques, the most common are interprocedurality and + * control flow sensitivity. + * + * @see SimpleStringOps + */ +public class Complex { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + /** + * Taken from com.sun.javafx.property.PropertyReference#reflect. + */ + @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "get-Hello, World-java.lang.Runtime") + @PartiallyConstant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(get-.*|get-Hello, World-java.lang.Runtime)") + @Failure(n = 0, levels = Level.L0) + @Invalid(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW) + @PartiallyConstant(n = 0, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "(get-.*|get-Hello, World-.*)") + public void complexDependencyResolve(String s, Class clazz) { + String properName = s.length() == 1 ? s.substring(0, 1) : + getHelloWorld() + "-" + getRuntimeClassName(); + String getterName = "get-" + properName; + Method m; + try { + m = clazz.getMethod(getterName); + System.out.println(m); + analyzeString(getterName); + } catch (NoSuchMethodException var13) { + } + } + + /** + * Taken from com.sun.prism.impl.ps.BaseShaderContext#getPaintShader and slightly adapted + */ + @Constant(n = 0, levels = Level.TRUTH, value = "Hello, World_paintName(_PAD|_REFLECT|_REPEAT)?(_AlphaTest)?") + @Failure(n = 0, levels = Level.L0) + // or-cases are currently not collapsed into simpler conditionals / or-cases using prefix checking + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, value = "((Hello, World_paintName|Hello, World_paintName_PAD|Hello, World_paintName_REFLECT|Hello, World_paintName_REPEAT)_AlphaTest|Hello, World_paintName|Hello, World_paintName_PAD|Hello, World_paintName_REFLECT|Hello, World_paintName_REPEAT)") + public void getPaintShader(boolean getPaintType, int spreadMethod, boolean alphaTest) { + String shaderName = getHelloWorld() + "_" + "paintName"; + if (getPaintType) { + if (spreadMethod == 0) { + shaderName = shaderName + "_PAD"; + } else if (spreadMethod == 1) { + shaderName = shaderName + "_REFLECT"; + } else if (spreadMethod == 2) { + shaderName = shaderName + "_REPEAT"; + } + } + if (alphaTest) { + shaderName = shaderName + "_AlphaTest"; + } + analyzeString(shaderName); + } + + @Failure(n = 0, levels = Level.TRUTH) + @Failure(n = 1, levels = Level.TRUTH) + public void unknownCharValue() { + int charCode = new Random().nextInt(200); + char c = (char) charCode; + String s = String.valueOf(c); + analyzeString(s); + + StringBuilder sb = new StringBuilder(); + sb.append(c); + analyzeString(sb.toString()); + } + + @Failure(n = 0, levels = { Level.L0, Level.L1 }) + @Constant(n = 0, levels = { Level.L2, Level.L3 }, value = "value") + public String cyclicDependencyTest(String s) { + String value = getProperty(s); + analyzeString(value); + return value; + } + + /** + * Methods are called that return a string but are not within this project => cannot / will not interpret + */ + @Dynamic(n = 0, levels = Level.TRUTH, value = "(.*)*") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }) + @Invalid(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW) + @Dynamic(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = ".*") + public void methodsOutOfScopeTest() throws FileNotFoundException { + File file = new File("my-file.txt"); + Scanner sc = new Scanner(file); + StringBuilder sb = new StringBuilder(); + while (sc.hasNextLine()) { + sb.append(sc.nextLine()); + } + analyzeString(sb.toString()); + + analyzeString(System.clearProperty("os.version")); + } + + private String getProperty(String name) { + if (name == null) { + return cyclicDependencyTest("default"); + } else { + return "value"; + } + } + + private String getRuntimeClassName() { + return "java.lang.Runtime"; + } + + private static String getHelloWorld() { + return "Hello, World"; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ExceptionalControlStructures.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ExceptionalControlStructures.java new file mode 100644 index 0000000000..1c1f396f3f --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/ExceptionalControlStructures.java @@ -0,0 +1,91 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Various tests that test general compatibility with the control flow generated by try-catch(-finally) statements. + *

+ * Since this type of statement can compile to multiple instances of calls that were initially defined in "finally" + * blocks, more sink calls may be generated than visible in the source code. + * + * @see SimpleStringOps + */ +public class ExceptionalControlStructures { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + @Invalid(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW) + @PartiallyConstant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "File Content:.*") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "File Content:") + @PartiallyConstant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(File Content:|File Content:.*)") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "File Content:") + @PartiallyConstant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(File Content:|File Content:.*)") + @Failure(n = 2, levels = Level.L0) + public void tryFinally(String filename) { + StringBuilder sb = new StringBuilder("File Content:"); + try { + String data = new String(Files.readAllBytes(Paths.get(filename))); + sb.append(data); + } catch (Exception ignore) { + } finally { + analyzeString(sb.toString()); + } + } + + @Invalid(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW) + @PartiallyConstant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "=====.*") + @Failure(n = 0, levels = Level.L0) + // Exception case without own thrown exception + @Constant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "==========") + @PartiallyConstant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(=====.*=====|==========)") + @Failure(n = 1, levels = Level.L0) + // The following cases are detected: + // 1. Code around Files.readAllBytes failing, throwing a non-exception Throwable -> no append (Pos 1) + // 2. Code around Files.readAllBytes failing, throwing an exception Throwable -> exception case append (Pos 4) + // 3. First append succeeds, throws no exception -> only first append (Pos 2) + // 4. First append is executed but throws an exception Throwable -> both appends (Pos 3) + @Constant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "(=====|==========)") + @PartiallyConstant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(=====|=====.*|=====.*=====|==========)") + @Failure(n = 2, levels = Level.L0) + public void tryCatchFinally(String filename) { + StringBuilder sb = new StringBuilder("====="); + try { + String data = new String(Files.readAllBytes(Paths.get(filename))); + sb.append(data); + } catch (Exception ignore) { + sb.append("====="); + } finally { + analyzeString(sb.toString()); + } + } + + @Invalid(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW) + @PartiallyConstant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "BOS:.*") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "BOS::EOS") + @PartiallyConstant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(BOS:.*:EOS|BOS::EOS)") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "BOS::EOS") + @PartiallyConstant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(BOS:.*:EOS|BOS::EOS)") + @Failure(n = 2, levels = Level.L0) + public void tryCatchFinallyWithThrowable(String filename) { + StringBuilder sb = new StringBuilder("BOS:"); + try { + String data = new String(Files.readAllBytes(Paths.get(filename))); + sb.append(data); + } catch (Throwable t) { + sb.append(":EOS"); + } finally { + analyzeString(sb.toString()); + } + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FieldAccesses.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FieldAccesses.java new file mode 100644 index 0000000000..1a194e32b4 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FieldAccesses.java @@ -0,0 +1,116 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +import java.util.Random; + +/** + * Various tests that test general compatibility with the field access information FPCF property, e.g. being able to + * analyze field reads and writes across method boundaries. + * + * @see SimpleStringOps + */ +public class FieldAccesses { + + protected String nonFinalNonStaticField = "private l0 non-final string field"; + public static String nonFinalStaticField = "will not be revealed here"; + public static final String finalStaticField = "mine"; + private String fieldWithSelfInit = "init field value"; + private static final String fieldWithSelfInitWithComplexInit; + private String fieldWithConstructorInit; + private float fieldWithConstructorParameterInit; + private String writeInSameMethodField; + private String noWriteField; + private Object unsupportedTypeField; + + static { + if (new Random().nextBoolean()) { + fieldWithSelfInitWithComplexInit = "Impl_Stub_1"; + } else { + fieldWithSelfInitWithComplexInit = "Impl_Stub_2"; + } + } + + public FieldAccesses(float e) { + fieldWithConstructorInit = "initialized by constructor"; + fieldWithConstructorParameterInit = e; + } + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + @Constant(n = 0, levels = Level.TRUTH, value = "private l0 non-final string field") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 }) + public void nonFinalFieldRead() { + analyzeString(nonFinalNonStaticField); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "will not be revealed here") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 }) + public void nonFinalStaticFieldRead() { + analyzeString(nonFinalStaticField); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "Field Value:mine") + @Failure(n = 0, levels = Level.L0) + public void publicFinalStaticFieldRead() { + StringBuilder sb = new StringBuilder("Field Value:"); + System.out.println(sb); + sb.append(finalStaticField); + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "init field value") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 }) + public void fieldWithInitRead() { + analyzeString(fieldWithSelfInit.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(Impl_Stub_1|Impl_Stub_2)") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 }) + public void fieldWithInitWithOutOfScopeRead() { + analyzeString(fieldWithSelfInitWithComplexInit); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "initialized by constructor") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 }) + public void fieldInitByConstructorRead() { + analyzeString(fieldWithConstructorInit.toString()); + } + + @Dynamic(n = 0, levels = Level.TRUTH, value = "^-?\\d*\\.{0,1}\\d+$") + @Failure(n = 0, levels = Level.L0) + @Failure(n = 0, levels = Level.L1, domains = DomainLevel.L1) + @Invalid(n = 0, levels = Level.L1, domains = DomainLevel.L2, soundness = SoundnessMode.LOW) + @Dynamic(n = 0, levels = Level.L1, domains = DomainLevel.L2, soundness = SoundnessMode.HIGH, + value = "^-?\\d*\\.{0,1}\\d+$", reason = "the field value is inlined using L2 domains") + @Invalid(n = 0, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW) + public void fieldInitByConstructorParameter() { + analyzeString(new StringBuilder().append(fieldWithConstructorParameterInit).toString()); + } + + // Contains a field write in the same method which cannot be captured by flow functions + @Constant(n = 0, levels = Level.TRUTH, value = "(some value|^null$)") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 }) + @Constant(n = 0, levels = Level.L3, soundness = SoundnessMode.LOW, value = "some value") + @Dynamic(n = 0, levels = Level.L3, soundness = SoundnessMode.HIGH, value = ".*") + public void fieldWriteInSameMethod() { + writeInSameMethodField = "some value"; + analyzeString(writeInSameMethodField); + } + + @Dynamic(n = 0, levels = Level.TRUTH, value = "(.*|^null$)") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2 }) + @Constant(n = 0, levels = Level.L3, soundness = SoundnessMode.LOW, value = "^null$") + public void fieldWithNoWriteTest() { + analyzeString(noWriteField); + } + + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }) + public void nonSupportedFieldTypeRead() { + analyzeString(unsupportedTypeField.toString()); + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionCalls.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionCalls.java new file mode 100644 index 0000000000..b681b1cd59 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionCalls.java @@ -0,0 +1,262 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.fixtures.string.tools.StringFactory; +import org.opalj.fpcf.fixtures.string.tools.ParameterDependentStringFactory; +import org.opalj.fpcf.fixtures.string.tools.StringProvider; +import org.opalj.fpcf.properties.string.*; + +/** + * Various tests that test specific compatibility of the different levels of the string analysis with resolving + * different types of function calls and their impact on the analyzed strings. As an example, arbitrary virtual function + * calls may be analyzable in one level of the string analysis but not another. + * + * @see SimpleStringOps + */ +public class FunctionCalls { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "java.lang.Object") + @Failure(n = 1, levels = Level.L0) + public void simpleStringConcatWithStaticFunctionCalls() { + analyzeString(StringProvider.concat("java.lang.", "String")); + analyzeString(StringProvider.concat("java.", StringProvider.concat("lang.", "Object"))); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.StringBuilder") + @Failure(n = 0, levels = { Level.L0, Level.L1 }) + public void fromFunctionCall() { + analyzeString(getStringBuilderClassName()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.StringBuilder") + @Failure(n = 0, levels = Level.L0) + @Invalid(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW) + @PartiallyConstant(n = 0, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "java.lang..*") + public void fromConstantAndFunctionCall() { + String className = "java.lang."; + System.out.println(className); + className += getSimpleStringBuilderClassName(); + analyzeString(className); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.Integer") + @Failure(n = 0, levels = Level.L0) + public void fromStaticMethodWithParamTest() { + analyzeString(StringProvider.getFQClassNameWithStringBuilder("java.lang", "Integer")); + } + + @Invalid(n = 0, levels = Level.TRUTH, reason = "the function has no return value, thus it does not return a string") + public void functionWithNoReturnValue() { + analyzeString(noReturnFunction()); + } + + /** Belongs to functionWithNoReturnValue. */ + public static String noReturnFunction() { + throw new RuntimeException(); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "Hello, World!") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "Hello, World?") + @Failure(n = 1, levels = { Level.L0, Level.L1 }) + public void functionWithFunctionParameter() { + analyzeString(addExclamationMark(getHelloWorld())); + analyzeString(addQuestionMark(getHelloWorld())); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(ERROR|java.lang.Object|java.lang.StringBuilder)") + @Constant(n = 0, levels = { Level.L0, Level.L1 }, soundness = SoundnessMode.LOW, value = "ERROR") + @Dynamic(n = 0, levels = { Level.L0, Level.L1 }, soundness = SoundnessMode.HIGH, value = "(.*|ERROR)") + public void simpleNonVirtualFunctionCallTestWithIf(int i) { + String s; + if (i == 0) { + s = getObjectClassName(); + } else if (i == 1) { + s = getStringBuilderClassName(); + } else { + s = "ERROR"; + } + analyzeString(s); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(ERROR|java.lang.Object|java.lang.StringBuilder)") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW, value = "ERROR") + @Dynamic(n = 0, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "(.*|ERROR)") + public void initFromNonVirtualFunctionCallTest(int i) { + String s; + if (i == 0) { + s = getObjectClassName(); + } else if (i == 1) { + s = getStringBuilderClassName(); + } else { + s = "ERROR"; + } + StringBuilder sb = new StringBuilder(s); + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "It is (Hello, World|great)") + @Failure(n = 0, levels = Level.L0) + public void appendWithTwoDefSitesWithFuncCallTest(int i) { + String s; + if (i > 0) { + s = "great"; + } else { + s = getHelloWorld(); + } + analyzeString(new StringBuilder("It is ").append(s).toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "Hello World") + @Failure(n = 0, levels = { Level.L0, Level.L1 }) + public void knownHierarchyInstanceTest() { + StringFactory sf = new ParameterDependentStringFactory(); + analyzeString(sf.getString("World")); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(Hello|Hello World)") + @Failure(n = 0, levels = { Level.L0, Level.L1 }) + public void unknownHierarchyInstanceTest(StringFactory stringFactory) { + analyzeString(stringFactory.getString("World")); + } + + /** + * A case where the single valid return value of the called function can be resolved without calling the function. + */ + @Constant(n = 0, levels = Level.TRUTH, value = "val") + @Failure(n = 0, levels = { Level.L0, Level.L1 }, domains = DomainLevel.L1) + @Constant(n = 0, levels = { Level.L2, Level.L3 }, domains = DomainLevel.L1, value = "(One|java.lang.Object|val)") + public void resolvableReturnValue() { + analyzeString(resolvableReturnValueFunction("val", 42)); + } + + /** + * Belongs to resolvableReturnValue. + */ + private String resolvableReturnValueFunction(String s, int i) { + switch (i) { + case 0: return getObjectClassName(); + case 1: return "One"; + default: return s; + } + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(One|java.lang.Object|val)") + @Failure(n = 0, levels = { Level.L0, Level.L1 }) + public void severalReturnValuesTest1() { + analyzeString(severalReturnValuesWithSwitchFunction("val", 42)); + } + + /** Belongs to severalReturnValuesTest1. */ + private String severalReturnValuesWithSwitchFunction(String s, int i) { + switch (i) { + case 0: return "One"; + case 1: return s; + default: return getObjectClassName(); + } + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(Hello, World|that's odd)") + @Failure(n = 0, levels = Level.L0) + public void severalReturnValuesTest2() { + analyzeString(severalReturnValuesWithIfElseFunction(42)); + } + + /** Belongs to severalReturnValuesTest2. */ + private static String severalReturnValuesWithIfElseFunction(int i) { + // The ternary operator would create only a single "return" statement which is not what we want here + if (i % 2 != 0) { + return "that's odd"; + } else { + return getHelloWorld(); + } + } + + @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "(Hello, World|my.helper.Class)") + @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|Hello, World|my.helper.Class)") + @Failure(n = 0, levels = Level.L0) + public String calleeWithFunctionParameter(String s, float i) { + analyzeString(s); + return s; + } + + @Constant(n = 0, levels = Level.TRUTH, value = "Hello, World") + @Failure(n = 0, levels = { Level.L0, Level.L1 }) + public void firstCallerForCalleeWithFunctionParameter() { + String s = calleeWithFunctionParameter(getHelloWorldProxy(), 900); + analyzeString(s); + } + + public void secondCallerForCalleeWithFunctionParameter() { + calleeWithFunctionParameter(getHelperClassProxy(), 900); + } + + @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "(Hello, World|my.helper.Class)") + @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|Hello, World|my.helper.Class)") + @Failure(n = 0, levels = Level.L0) + public String calleeWithFunctionParameterMultipleCallsInSameMethodTest(String s, float i) { + analyzeString(s); + return s; + } + + public void callerForCalleeWithFunctionParameterMultipleCallsInSameMethodTest() { + calleeWithFunctionParameterMultipleCallsInSameMethodTest(getHelloWorldProxy(), 900); + calleeWithFunctionParameterMultipleCallsInSameMethodTest(getHelperClassProxy(), 900); + } + + @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "(string.1|string.2)") + @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|string.1|string.2)") + public String calleeWithStringParameterMultipleCallsInSameMethodTest(String s, float i) { + analyzeString(s); + return s; + } + + public void callerForCalleeWithStringParameterMultipleCallsInSameMethodTest() { + calleeWithStringParameterMultipleCallsInSameMethodTest("string.1", 900); + calleeWithStringParameterMultipleCallsInSameMethodTest("string.2", 900); + } + + public static String getHelloWorldProxy() { + return getHelloWorld(); + } + + public static String getHelperClassProxy() { + return getHelperClass(); + } + + private static String getHelloWorld() { + return "Hello, World"; + } + + private static String getHelperClass() { + return "my.helper.Class"; + } + + private String getStringBuilderClassName() { + return "java.lang.StringBuilder"; + } + + private String getSimpleStringBuilderClassName() { + return "StringBuilder"; + } + + private String getObjectClassName() { + return "java.lang.Object"; + } + + private static String addExclamationMark(String s) { + return s + "!"; + } + + private String addQuestionMark(String s) { + return s + "?"; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionParameter.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionParameter.java new file mode 100644 index 0000000000..7d4f9d899a --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/FunctionParameter.java @@ -0,0 +1,58 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +/** + * Various tests that test whether detection of needing to resolve parameters for a given string works or not. Note that + * there is a separate test file for various function calls that also partially covers this detection. + * + * @see FunctionCalls + * @see SimpleStringOps + */ +public class FunctionParameter { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + public void parameterCaller() { + this.parameterRead("some-param-value", new StringBuilder("some-other-param-value")); + } + + @Constant(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "some-param-value") + @Dynamic(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|some-param-value)", + reason = "method is an entry point and thus has callers with unknown context") + @Constant(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "some-other-param-value") + @Dynamic(n = 1, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "(.*|some-other-param-value)", + reason = "method is an entry point and thus has callers with unknown context") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "value=some-param-value") + @PartiallyConstant(n = 2, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "value=(.*|some-param-value)", + reason = "method is an entry point and thus has callers with unknown context") + @Failure(n = 2, levels = Level.L0) + @Constant(n = 3, levels = Level.TRUTH, soundness = SoundnessMode.LOW, value = "value=some-param-value-some-other-param-value") + @PartiallyConstant(n = 3, levels = Level.TRUTH, soundness = SoundnessMode.HIGH, value = "value=(.*|some-param-value)-(.*|some-other-param-value)", + reason = "method is an entry point and thus has callers with unknown context") + @Failure(n = 3, levels = Level.L0) + public void parameterRead(String stringValue, StringBuilder sbValue) { + analyzeString(stringValue); + analyzeString(sbValue.toString()); + + StringBuilder sb = new StringBuilder("value="); + System.out.println(sb.toString()); + sb.append(stringValue); + analyzeString(sb.toString()); + + sb.append("-"); + sb.append(sbValue.toString()); + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String") + public void noParameterInformationRequiredTest(String s) { + System.out.println(s); + analyzeString("java.lang.String"); + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Loops.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Loops.java new file mode 100644 index 0000000000..1fb420a1d6 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Loops.java @@ -0,0 +1,280 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Random; + +/** + * Various tests that contain some kind of loops which modify string variables, requiring data flow analysis to resolve + * these values or at least approximate them. Currently, the string analysis either only interprets the loop body once + * (in low-soundness mode) or over-approximates with "any string" (in high-soundness mode). + * + * @see SimpleStringOps + */ +public class Loops { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + /** + * Simple for loops with known and unknown bounds. Note that no analysis supports loops yet. + */ + @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "a(b)*") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab") + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*") + @PartiallyConstant(n = 1, levels = Level.TRUTH, value = "a(b)*") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab") + @Dynamic(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*") + public void simpleForLoopWithKnownBounds() { + StringBuilder sb = new StringBuilder("a"); + for (int i = 0; i < 10; i++) { + sb.append("b"); + } + analyzeString(sb.toString()); + + int limit = new Random().nextInt(); + sb = new StringBuilder("a"); + for (int i = 0; i < limit; i++) { + sb.append("b"); + } + analyzeString(sb.toString()); + } + + @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "((x|^-?\\d+$))*yz") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "xyz") + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(.*|.*yz)") + public void ifElseInLoopWithAppendAfterwards() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + if (i % 2 == 0) { + sb.append("x"); + } else { + sb.append(i + 1); + } + } + sb.append("yz"); + + analyzeString(sb.toString()); + } + + @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "a(b)*") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab") + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*") + public void nestedLoops(int range) { + for (int i = 0; i < range; i++) { + StringBuilder sb = new StringBuilder("a"); + for (int j = 0; j < range * range; j++) { + sb.append("b"); + } + analyzeString(sb.toString()); + } + } + + @PartiallyConstant(n = 0, value = "((x|^-?\\d+$))*yz", levels = Level.TRUTH) + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "xyz") + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(.*|.*yz)") + public void stringBufferExample() { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 20; i++) { + if (i % 2 == 0) { + sb.append("x"); + } else { + sb.append(i + 1); + } + } + sb.append("yz"); + + analyzeString(sb.toString()); + } + + @PartiallyConstant(n = 0, value = "a(b)*", levels = Level.TRUTH) + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab") + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*") + public void whileTrueWithBreak() { + StringBuilder sb = new StringBuilder("a"); + while (true) { + sb.append("b"); + if (sb.length() > 100) { + break; + } + } + analyzeString(sb.toString()); + } + + @PartiallyConstant(n = 0, value = "a(b)*", levels = Level.TRUTH) + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "ab") + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*") + public void whileNonTrueWithBreak(int i) { + StringBuilder sb = new StringBuilder("a"); + int j = 0; + while (j < i) { + sb.append("b"); + if (sb.length() > 100) { + break; + } + j++; + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(iv1|iv2): ") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "(iv1|iv2): ") + // The real value is not fully resolved yet, since the string builder is used in a while loop, + // which leads to the string builder potentially carrying any value. This can be refined by + // recording pc specific states during data flow analysis. + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "((iv1|iv2): |.*)") + @PartiallyConstant(n = 1, levels = Level.TRUTH, value = "(iv1|iv2): ((great!)?)*(java.lang.Runtime)?") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 1, levels = Level.L1, soundness = SoundnessMode.LOW, value = "(iv1|iv2): great!") + @Dynamic(n = 1, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "(.*|.*.*)") + @Constant(n = 1, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "((iv1|iv2): great!|(iv1|iv2): great!java.lang.Runtime)") + @Dynamic(n = 1, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(.*|.*java.lang.Runtime)") + public void extensiveWithManyControlStructures(boolean cond) { + StringBuilder sb = new StringBuilder(); + if (cond) { + sb.append("iv1"); + } else { + sb.append("iv2"); + } + System.out.println(sb); + sb.append(": "); + + analyzeString(sb.toString()); + + Random random = new Random(); + while (random.nextFloat() > 5.) { + if (random.nextInt() % 2 == 0) { + sb.append("great!"); + } + } + + if (sb.indexOf("great!") > -1) { + sb.append(getRuntimeClassName()); + } + + analyzeString(sb.toString()); + } + + // The bytecode produces an "if" within an "if" inside the first loop => two conditions + @Constant(n = 0, levels = Level.TRUTH, value = "abc((d)?)*") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "(abc|abcd)") + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*") + @Constant(n = 1, levels = Level.TRUTH, value = "") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "") + @Dynamic(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(|.*)") + @Dynamic(n = 2, levels = Level.TRUTH, value = "((.*)?)*") + @Failure(n = 2, levels = Level.L0) + @Constant(n = 2, levels = Level.L1, soundness = SoundnessMode.LOW, value = "") + @Constant(n = 2, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "(|java.lang.Runtime)") + @Dynamic(n = 2, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*") + public void breakContinueExamples(int value) { + StringBuilder sb1 = new StringBuilder("abc"); + for (int i = 0; i < value; i++) { + if (i % 7 == 1) { + break; + } else if (i % 3 == 0) { + continue; + } else { + sb1.append("d"); + } + } + analyzeString(sb1.toString()); + + StringBuilder sb2 = new StringBuilder(""); + for (int i = 0; i < value; i++) { + if (i % 2 == 0) { + break; + } + sb2.append("some_value"); + } + analyzeString(sb2.toString()); + + StringBuilder sb3 = new StringBuilder(); + for (int i = 0; i < 10; i++) { + if (sb3.toString().equals("")) { + // The analysis currently does not detect, that this statement is executed at + // most / exactly once as it fully relies on the three-address code and does not + // infer any semantics of conditionals + sb3.append(getRuntimeClassName()); + } else { + continue; + } + } + analyzeString(sb3.toString()); + } + + /** + * Some comprehensive example for experimental purposes taken from the JDK and slightly modified + */ + @Constant(n = 0, levels = Level.TRUTH, value = "Hello: (java.lang.Runtime|java.lang.StringBuilder|StringBuilder)?") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW, value = "Hello: ") + @Constant(n = 0, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, + value = "(Hello: |Hello: StringBuilder|Hello: java.lang.Runtime|Hello: java.lang.StringBuilder)") + @Dynamic(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = ".*") + protected void setDebugFlags(String[] var1) { + for(int var2 = 0; var2 < var1.length; ++var2) { + String var3 = var1[var2]; + + int randomValue = new Random().nextInt(); + StringBuilder sb = new StringBuilder("Hello: "); + if (randomValue % 2 == 0) { + sb.append(getRuntimeClassName()); + } else if (randomValue % 3 == 0) { + sb.append(getStringBuilderClassName()); + } else if (randomValue % 4 == 0) { + sb.append(getSimpleStringBuilderClassName()); + } + + try { + Field var4 = this.getClass().getField(var3 + "DebugFlag"); + int var5 = var4.getModifiers(); + if (Modifier.isPublic(var5) && !Modifier.isStatic(var5) && + var4.getType() == Boolean.TYPE) { + var4.setBoolean(this, true); + } + } catch (IndexOutOfBoundsException var90) { + System.out.println("Should never happen!"); + } catch (Exception var6) { + int i = 10; + i += new Random().nextInt(); + System.out.println("Some severe error occurred!" + i); + } finally { + int i = 10; + i += new Random().nextInt(); + if (i % 2 == 0) { + System.out.println("Ready to analyze now in any case!" + i); + } + } + + analyzeString(sb.toString()); + } + } + + private String getRuntimeClassName() { + return "java.lang.Runtime"; + } + + private String getStringBuilderClassName() { + return "java.lang.StringBuilder"; + } + + private String getSimpleStringBuilderClassName() { + return "StringBuilder"; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Result.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Result.java new file mode 100644 index 0000000000..91ccd9a6ab --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/Result.java @@ -0,0 +1,30 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +/** + * Tests compatibility of the results of the string analysis with e.g. being compiled to a regex string. + * + * @see SimpleStringOps + */ +public class Result { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + @Constant(n = 0, levels = Level.TRUTH, value = "\\[B") + @Constant(n = 1, levels = Level.TRUTH, value = "\\[Ljava.lang.String;") + @Constant(n = 2, levels = Level.TRUTH, value = "\\[\\[Lsun.security.pkcs.SignerInfo;") + @Constant(n = 3, levels = Level.TRUTH, value = "US\\$") + @Constant(n = 4, levels = Level.TRUTH, value = "US\\\\") + public void regexCompilableTest() { + analyzeString("[B"); + analyzeString("[Ljava.lang.String;"); + analyzeString("[[Lsun.security.pkcs.SignerInfo;"); + analyzeString("US$"); + analyzeString("US\\"); + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleControlStructures.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleControlStructures.java new file mode 100644 index 0000000000..f4d2fd3e17 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleControlStructures.java @@ -0,0 +1,266 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +import java.util.Random; + +/** + * Various tests that test compatibility of the data flow analysis with simple control structures like if-statements. + * + * @see SimpleStringOps + */ +public class SimpleControlStructures { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + @Dynamic(n = 0, levels = Level.TRUTH, value = "(^-?\\d+$|x)") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "x") + @Constant(n = 1, levels = Level.TRUTH, value = "(42-42|x)") + @Failure(n = 1, levels = Level.L0) + public void ifElseWithStringBuilderWithIntExpr() { + StringBuilder sb1 = new StringBuilder(); + StringBuilder sb2 = new StringBuilder(); + int i = new Random().nextInt(); + if (i % 2 == 0) { + sb1.append("x"); + sb2.append(42); + sb2.append(-42); + } else { + sb1.append(i + 1); + sb2.append("x"); + } + analyzeString(sb1.toString()); + analyzeString(sb2.toString()); + } + + @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "(3.142.71828|^-?\\d*\\.{0,1}\\d+$2.71828)") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 0, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "3.142.71828") + public void ifElseWithStringBuilderWithFloatExpr() { + StringBuilder sb1 = new StringBuilder(); + int i = new Random().nextInt(); + if (i % 2 == 0) { + sb1.append(3.14); + } else { + sb1.append(new Random().nextFloat()); + } + float e = (float) 2.71828; + sb1.append(e); + analyzeString(sb1.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(a|b)") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "(ab|ac)") + @Failure(n = 1, levels = Level.L0) + public void ifElseWithStringBuilder() { + StringBuilder sb1; + StringBuilder sb2 = new StringBuilder("a"); + + int i = new Random().nextInt(); + if (i % 2 == 0) { + sb1 = new StringBuilder("a"); + sb2.append("b"); + } else { + sb1 = new StringBuilder("b"); + sb2.append("c"); + } + analyzeString(sb1.toString()); + analyzeString(sb2.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(abcd|axyz)") + @Failure(n = 0, levels = Level.L0) + public void ifElseWithStringBuilderWithMultipleAppends() { + StringBuilder sb = new StringBuilder("a"); + int i = new Random().nextInt(); + if (i % 2 == 0) { + sb.append("b"); + sb.append("c"); + sb.append("d"); + } else { + sb.append("x"); + sb.append("y"); + sb.append("z"); + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(a|abcd|axyz)") + @Failure(n = 0, levels = Level.L0) + public void ifElseWithStringBuilderWithMultipleAppendsAndNonUsedElseIf() { + StringBuilder sb = new StringBuilder("a"); + int i = new Random().nextInt(); + if (i % 3 == 0) { + sb.append("b"); + sb.append("c"); + sb.append("d"); + } else if (i % 2 == 0) { + System.out.println("something"); + } else { + sb.append("x"); + sb.append("y"); + sb.append("z"); + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab)") + @Failure(n = 0, levels = Level.L0) + public void ifWithoutElse() { + StringBuilder sb = new StringBuilder("a"); + int i = new Random().nextInt(); + if (i % 2 == 0) { + sb.append("b"); + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.Runtime") + @Failure(n = 0, levels = Level.L0) + public void ifConditionAppendsToString(String className) { + StringBuilder sb = new StringBuilder(); + if (sb.append("java.lang.Runtime").toString().equals(className)) { + System.out.println("Yep, got the correct class!"); + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab|ac)") + @Failure(n = 0, levels = Level.L0) + public void switchRelevantAndIrrelevant(int value) { + StringBuilder sb = new StringBuilder("a"); + switch (value) { + case 0: + sb.append("b"); + break; + case 1: + sb.append("c"); + break; + case 3: + break; + case 4: + break; + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab|ac|ad)") + @Failure(n = 0, levels = Level.L0) + public void switchRelevantAndIrrelevantWithRelevantDefault(int value) { + StringBuilder sb = new StringBuilder("a"); + switch (value) { + case 0: + sb.append("b"); + break; + case 1: + sb.append("c"); + break; + case 2: + break; + case 3: + break; + default: + sb.append("d"); + break; + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab|ac)") + @Failure(n = 0, levels = Level.L0) + public void switchRelevantAndIrrelevantWithIrrelevantDefault(int value) { + StringBuilder sb = new StringBuilder("a"); + switch (value) { + case 0: + sb.append("b"); + break; + case 1: + sb.append("c"); + break; + case 2: + break; + case 3: + break; + default: + break; + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(ab|ac|ad)") + @Failure(n = 0, levels = Level.L0) + public void switchRelevantWithRelevantDefault(int value) { + StringBuilder sb = new StringBuilder("a"); + switch (value) { + case 0: + sb.append("b"); + break; + case 1: + sb.append("c"); + break; + default: + sb.append("d"); + break; + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(a|ab|ac|ad|af)") + @Failure(n = 0, levels = Level.L0) + public void switchNestedNoNestedDefault(int value, int value2) { + StringBuilder sb = new StringBuilder("a"); + switch (value) { + case 0: + sb.append("b"); + break; + case 1: + switch (value2) { + case 0: + sb.append("c"); + break; + case 1: + sb.append("d"); + break; + } + break; + default: + sb.append("f"); + break; + } + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(ab|ac|ad|ae|af)") + @Failure(n = 0, levels = Level.L0) + public void switchNestedWithNestedDefault(int value, int value2) { + StringBuilder sb = new StringBuilder("a"); + switch (value) { + case 0: + sb.append("b"); + break; + case 1: + switch (value2) { + case 0: + sb.append("c"); + break; + case 1: + sb.append("d"); + break; + default: + sb.append("e"); + break; + } + break; + default: + sb.append("f"); + break; + } + analyzeString(sb.toString()); + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringBuilderOps.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringBuilderOps.java new file mode 100644 index 0000000000..2d10463538 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringBuilderOps.java @@ -0,0 +1,194 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Various tests that test compatibility with selected methods defined on string builders and string buffers, such as + * append, reset etc. + * + * @see SimpleStringOps + */ +public class SimpleStringBuilderOps { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + public void analyzeString(StringBuilder sb) {} + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String") + @Failure(n = 0, levels = Level.L0) + public void multipleDirectAppends() { + StringBuilder sb = new StringBuilder("java"); + sb.append(".").append("lang").append(".").append("String"); + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "SomeOther") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "SomeOther") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, value = "(Some|SomeOther)") + public void stringValueOfWithStringBuilder() { + StringBuilder sb = new StringBuilder("Some"); + sb.append("Other"); + analyzeString(String.valueOf(sb)); + + analyzeString(sb); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "Some") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "Other") + @Failure(n = 1, levels = Level.L0) + public void stringBuilderBufferInitArguments() { + StringBuilder sb = new StringBuilder("Some"); + analyzeString(sb.toString()); + + StringBuffer sb2 = new StringBuffer("Other"); + analyzeString(sb2.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.StringBuilder") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "java.lang.StringBuilder") + @Failure(n = 1, levels = Level.L0) + public void simpleClearExamples() { + StringBuilder sb1 = new StringBuilder("init_value:"); + sb1.setLength(0); + sb1.append("java.lang.StringBuilder"); + + StringBuilder sb2 = new StringBuilder("init_value:"); + System.out.println(sb2.toString()); + sb2 = new StringBuilder(); + sb2.append("java.lang.StringBuilder"); + + analyzeString(sb1.toString()); + analyzeString(sb2.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(Goodbye|init_value:Hello, world!Goodbye)") + @Failure(n = 0, levels = Level.L0) + public void advancedClearExampleWithSetLength(int value) { + StringBuilder sb = new StringBuilder("init_value:"); + if (value < 10) { + sb.setLength(0); + } else { + sb.append("Hello, world!"); + } + sb.append("Goodbye"); + analyzeString(sb.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "replaced_value") + @Failure(n = 0, levels = { Level.L0, Level.L1, Level.L2, Level.L3 }) + @Constant(n = 1, levels = Level.TRUTH, value = "(...:Goodbye|init_value:Hello, world!Goodbye)") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, + value = "init_value:Hello, world!Goodbye") + @PartiallyConstant(n = 1, levels = { Level.L1, Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, + value = "(.*Goodbye|init_value:Hello, world!Goodbye)") + public void replaceExamples(int value) { + StringBuilder sb1 = new StringBuilder("init_value"); + sb1.replace(0, 5, "replaced_"); + analyzeString(sb1.toString()); + + sb1 = new StringBuilder("init_value:"); + if (value < 10) { + sb1.replace(0, value, "..."); + } else { + sb1.append("Hello, world!"); + } + sb1.append("Goodbye"); + analyzeString(sb1.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "B.") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "java.langStringB.") + @Failure(n = 1, levels = Level.L0) + public void directAppendConcatsWith2ndStringBuilder() { + StringBuilder sb = new StringBuilder("java"); + StringBuilder sb2 = new StringBuilder("B"); + sb.append('.').append("lang"); + sb2.append('.'); + sb.append("String"); + sb.append(sb2.toString()); + analyzeString(sb2.toString()); + analyzeString(sb.toString()); + } + + /** + * Checks if the value of a string builder that depends on the complex construction of a second one can be determined. + */ + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.(Object|Runtime)") + @Failure(n = 0, levels = Level.L0) + public void complexSecondStringBuilderRead(String className) { + StringBuilder sbObj = new StringBuilder("Object"); + StringBuilder sbRun = new StringBuilder("Runtime"); + + StringBuilder sb1 = new StringBuilder(); + if (sb1.length() == 0) { + sb1.append(sbObj.toString()); + } else { + sb1.append(sbRun.toString()); + } + + StringBuilder sb2 = new StringBuilder("java.lang."); + sb2.append(sb1.toString()); + analyzeString(sb2.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(java.lang.Object|java.lang.Runtime)") + @Failure(n = 0, levels = Level.L0) + public void simpleSecondStringBuilderRead(String className) { + StringBuilder sbObj = new StringBuilder("Object"); + StringBuilder sbRun = new StringBuilder("Runtime"); + + StringBuilder sb1 = new StringBuilder("java.lang."); + if (sb1.length() == 0) { + sb1.append(sbObj.toString()); + } else { + sb1.append(sbRun.toString()); + } + + analyzeString(sb1.toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(Object|ObjectRuntime)") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "(Runtime|RuntimeObject)") + @Failure(n = 1, levels = Level.L0) + public void crissCrossExample(String className) { + StringBuilder sbObj = new StringBuilder("Object"); + StringBuilder sbRun = new StringBuilder("Runtime"); + + if (className.length() == 0) { + sbRun.append(sbObj.toString()); + } else { + sbObj.append(sbRun.toString()); + } + + analyzeString(sbObj.toString()); + analyzeString(sbRun.toString()); + } + + @Invalid(n = 0, levels = Level.TRUTH, soundness = SoundnessMode.LOW) + @PartiallyConstant(n = 0, levels = Level.TRUTH, value = "File Content:.*", soundness = SoundnessMode.HIGH) + @Failure(n = 0, levels = Level.L0) + public void withUnknownAppendSource(String filename) throws IOException { + StringBuilder sb = new StringBuilder("File Content:"); + String data = new String(Files.readAllBytes(Paths.get(filename))); + sb.append(data); + analyzeString(sb.toString()); + } + + // IMPROVE Add the following tests + // - Passing string builders as call parameters should result in a failure (called method can modify string builder) + // - Support generic function calls, in particular with upper return type bounds that are non-object +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringOps.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringOps.java new file mode 100644 index 0000000000..ce890f5686 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SimpleStringOps.java @@ -0,0 +1,242 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.*; + +/** + * All files in this package define various tests for the string analysis. The following things are to be considered + * when adding test cases: + * + *

+ *

+ * This file defines various tests related to simple operations on strings and presence of multiple def sites of such + * strings. + * + * @author Maximilian Rüsch + */ +public class SimpleStringOps { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String") + @Constant(n = 1, levels = Level.TRUTH, value = "java.lang.String") + public void constantStringReads() { + analyzeString("java.lang.String"); + + String className = "java.lang.String"; + analyzeString(className); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "c") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "42.3") + @Failure(n = 1, levels = Level.L0) + @Constant(n = 2, levels = Level.TRUTH, value = "java.lang.Runtime") + @Failure(n = 2, levels = { Level.L0, Level.L1 }) + public void valueOfTest() { + analyzeString(String.valueOf('c')); + analyzeString(String.valueOf((float) 42.3)); + analyzeString(String.valueOf(getRuntimeClassName())); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "java.lang.String") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "java.lang.Object") + @Failure(n = 1, levels = Level.L0) + public void simpleStringConcat() { + String className1 = "java.lang."; + System.out.println(className1); + className1 += "String"; + analyzeString(className1); + + String className2 = "java."; + System.out.println(className2); + className2 += "lang."; + System.out.println(className2); + className2 += "Object"; + analyzeString(className2); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "va.") + @Failure(n = 0, levels = Level.L0) + @Constant(n = 1, levels = Level.TRUTH, value = "va.lang.") + @Failure(n = 1, levels = Level.L0) + public void simpleSubstring() { + String someString = "java.lang."; + analyzeString(someString.substring(2, 5)); + analyzeString(someString.substring(2)); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(java.lang.Runtime|java.lang.System)") + public void multipleConstantDefSites(boolean cond) { + String s; + if (cond) { + s = "java.lang.System"; + } else { + s = "java.lang.Runtime"; + } + analyzeString(s); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "It is (great|not great)") + @Failure(n = 0, levels = Level.L0) + public void appendWithTwoDefSites(int i) { + String s; + if (i > 0) { + s = "great"; + } else { + s = "not great"; + } + analyzeString(new StringBuilder("It is ").append(s).toString()); + } + + @Constant(n = 0, levels = Level.TRUTH, value = "(Some|SomeOther)") + @Constant(n = 0, levels = Level.L0, soundness = SoundnessMode.LOW, value = "Some") + @Dynamic(n = 0, levels = Level.L0, soundness = SoundnessMode.HIGH, value = "(.*|Some)") + @Constant(n = 1, levels = Level.TRUTH, value = "(Impostor|Some)") + @Constant(n = 2, levels = Level.TRUTH, value = "(SomeImpostor|SomeOther)") + @Failure(n = 2, levels = Level.L0) + public void ternaryOperators(boolean flag) { + String s1 = "Some"; + String s2 = s1 + "Other"; + String s3 = "Impostor"; + + analyzeString(flag ? s1 : s2); + analyzeString(flag ? s1 : s3); + analyzeString(flag ? s1 + s3 : s2); + } + + /** + * A more comprehensive case where multiple definition sites have to be considered each with a different string + * generation mechanism + */ + @Constant(n = 0, levels = Level.TRUTH, value = "(java.lang.Object|java.lang.Runtime|java.lang.System|java.lang.StringBuilder)") + @Constant(n = 0, levels = Level.L0, soundness = SoundnessMode.LOW, value = "java.lang.System") + @Dynamic(n = 0, levels = Level.L0, soundness = SoundnessMode.HIGH, value = "(.*|java.lang.System)") + @Constant(n = 0, levels = Level.L1, soundness = SoundnessMode.LOW, value = "java.lang.System") + @Dynamic(n = 0, levels = Level.L1, soundness = SoundnessMode.HIGH, value = "(.*|java.lang..*|java.lang.System)") + @Constant(n = 0, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.LOW, value = "(java.lang.StringBuilder|java.lang.StringBuilder|java.lang.System)") + @Dynamic(n = 0, levels = { Level.L2, Level.L3 }, soundness = SoundnessMode.HIGH, value = "(.*|java.lang.StringBuilder|java.lang.StringBuilder|java.lang.System)") + public void multipleDefSites(int value) { + String[] arr = new String[] { "java.lang.Object", getRuntimeClassName() }; + + String s; + switch (value) { + case 0: + s = arr[value]; + break; + case 1: + s = arr[value]; + break; + case 3: + s = "java.lang.System"; + break; + case 4: + s = "java.lang." + getSimpleStringBuilderClassName(); + break; + default: + s = getStringBuilderClassName(); + } + + analyzeString(s); + } + + private String getRuntimeClassName() { + return "java.lang.Runtime"; + } + + private String getStringBuilderClassName() { + return "java.lang.StringBuilder"; + } + + private String getSimpleStringBuilderClassName() { + return "StringBuilder"; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SystemProperties.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SystemProperties.java new file mode 100644 index 0000000000..a328619b4c --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/SystemProperties.java @@ -0,0 +1,27 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string; + +import org.opalj.fpcf.properties.string.Constant; +import org.opalj.fpcf.properties.string.Failure; +import org.opalj.fpcf.properties.string.Level; + +/** + * Tests the integration with the system properties FPCF property. + * + * @see SimpleStringOps + */ +public class SystemProperties { + + /** + * Serves as the sink for string variables to be analyzed. + */ + public void analyzeString(String s) {} + + @Constant(n = 0, levels = Level.TRUTH, value = "some.test.value") + @Failure(n = 0, levels = Level.L0) + public void systemPropertiesIntegrationTest() { + System.setProperty("some.test.property", "some.test.value"); + String s = System.getProperty("some.test.property"); + analyzeString(s); + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/ParameterDependentStringFactory.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/ParameterDependentStringFactory.java new file mode 100644 index 0000000000..198d514828 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/ParameterDependentStringFactory.java @@ -0,0 +1,11 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string.tools; + +public class ParameterDependentStringFactory implements StringFactory { + + @Override + public String getString(String parameter) { + return "Hello " + parameter; + } + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/SimpleStringFactory.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/SimpleStringFactory.java new file mode 100644 index 0000000000..f8fe33378d --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/SimpleStringFactory.java @@ -0,0 +1,11 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string.tools; + +public class SimpleStringFactory implements StringFactory { + + @Override + public String getString(String parameter) { + return "Hello"; + } + +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringFactory.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringFactory.java new file mode 100644 index 0000000000..e611cf693f --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringFactory.java @@ -0,0 +1,9 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string.tools; + +public interface StringFactory { + + String getString(String parameter); + +} + diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringProvider.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringProvider.java new file mode 100644 index 0000000000..a326374a31 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/string/tools/StringProvider.java @@ -0,0 +1,23 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.string.tools; + +public class StringProvider { + + /** + * Returns "[packageName].[className]". + */ + public static String concat(String firstString, String secondString) { + return firstString + secondString; + } + + /** + * Returns "[packageName].[className]". + */ + public static String getFQClassNameWithStringBuilder(String packageName, String className) { + return (new StringBuilder()).append(packageName).append(".").append(className).toString(); + } + + public static String getSomeValue() { + return "someValue"; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constant.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constant.java new file mode 100644 index 0000000000..72437d501b --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constant.java @@ -0,0 +1,33 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import org.opalj.fpcf.properties.PropertyValidator; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@PropertyValidator(key = "StringConstancy", validator = ConstantStringMatcher.class) +@Documented +@Repeatable(Constants.class) +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.LOCAL_VARIABLE }) +public @interface Constant { + + int n(); + + String reason() default "N/A"; + + /** + * A regexp like string that describes the element(s) that are expected. + */ + String value(); + + Level[] levels(); + + DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 }; + + SoundnessMode[] soundness() default { SoundnessMode.LOW, SoundnessMode.HIGH }; +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constants.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constants.java new file mode 100644 index 0000000000..4be7497463 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Constants.java @@ -0,0 +1,16 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface Constants { + + Constant[] value(); +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/DomainLevel.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/DomainLevel.java new file mode 100644 index 0000000000..0b6330375f --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/DomainLevel.java @@ -0,0 +1,22 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +public enum DomainLevel { + + L1("L1"), + L2("L2"); + + private final String value; + + DomainLevel(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamic.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamic.java new file mode 100644 index 0000000000..f6a422e9c6 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamic.java @@ -0,0 +1,33 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import org.opalj.fpcf.properties.PropertyValidator; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@PropertyValidator(key = "StringConstancy", validator = DynamicStringMatcher.class) +@Documented +@Repeatable(Dynamics.class) +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface Dynamic { + + int n(); + + String reason() default "N/A"; + + /** + * A regexp like string that describes the element(s) that are expected. + */ + String value(); + + Level[] levels(); + + DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 }; + + SoundnessMode[] soundness() default { SoundnessMode.LOW, SoundnessMode.HIGH }; +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamics.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamics.java new file mode 100644 index 0000000000..ab0b34b717 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Dynamics.java @@ -0,0 +1,16 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface Dynamics { + + Dynamic[] value(); +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failure.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failure.java new file mode 100644 index 0000000000..505c8d3a65 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failure.java @@ -0,0 +1,25 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import java.lang.annotation.*; + +/** + * Note that this annotation will be rewritten into {@link Invalid} or {@link Dynamic} depending on the soundness mode. + * + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@Documented +@Repeatable(Failures.class) +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface Failure { + + int n(); + + Level[] levels(); + + String reason() default "N/A"; + + DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 }; +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failures.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failures.java new file mode 100644 index 0000000000..1a8c57bcbb --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Failures.java @@ -0,0 +1,16 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface Failures { + + Failure[] value(); +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalid.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalid.java new file mode 100644 index 0000000000..d19f597681 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalid.java @@ -0,0 +1,28 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import org.opalj.fpcf.properties.PropertyValidator; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@PropertyValidator(key = "StringConstancy", validator = InvalidStringMatcher.class) +@Documented +@Repeatable(Invalids.class) +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface Invalid { + + int n(); + + String reason() default "N/A"; + + Level[] levels(); + + DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 }; + + SoundnessMode[] soundness() default { SoundnessMode.LOW, SoundnessMode.HIGH }; +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalids.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalids.java new file mode 100644 index 0000000000..a47be759ca --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Invalids.java @@ -0,0 +1,16 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface Invalids { + + Invalid[] value(); +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Level.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Level.java new file mode 100644 index 0000000000..cec1133a6f --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/Level.java @@ -0,0 +1,25 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +public enum Level { + + TRUTH("TRUTH"), + L0("L0"), + L1("L1"), + L2("L2"), + L3("L3"); + + private final String value; + + Level(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstant.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstant.java new file mode 100644 index 0000000000..53f9178533 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstant.java @@ -0,0 +1,33 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import org.opalj.fpcf.properties.PropertyValidator; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@PropertyValidator(key = "StringConstancy", validator = PartiallyConstantStringMatcher.class) +@Documented +@Repeatable(PartiallyConstants.class) +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface PartiallyConstant { + + int n(); + + String reason() default "N/A"; + + /** + * A regexp like string that describes the element(s) that are expected. + */ + String value(); + + Level[] levels(); + + DomainLevel[] domains() default { DomainLevel.L1, DomainLevel.L2 }; + + SoundnessMode[] soundness() default { SoundnessMode.LOW, SoundnessMode.HIGH }; +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstants.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstants.java new file mode 100644 index 0000000000..ca5f0233ff --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/PartiallyConstants.java @@ -0,0 +1,16 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +import java.lang.annotation.*; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface PartiallyConstants { + + PartiallyConstant[] value(); +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/SoundnessMode.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/SoundnessMode.java new file mode 100644 index 0000000000..8494678f99 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/string/SoundnessMode.java @@ -0,0 +1,22 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.properties.string; + +/** + * @see org.opalj.fpcf.fixtures.string.SimpleStringOps + * @author Maximilian Rüsch + */ +public enum SoundnessMode { + + LOW("LOW"), + HIGH("HIGH"); + + private final String value; + + SoundnessMode(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PropertiesTest.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PropertiesTest.scala index fef3b9b5d3..9ed2f1476d 100644 --- a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PropertiesTest.scala +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PropertiesTest.scala @@ -361,33 +361,42 @@ abstract class PropertiesTest extends AnyFunSpec with Matchers { } def executeAnalyses( - analysisRunners: Iterable[ComputationSpecification[FPCFAnalysis]] + analysisRunners: Iterable[ComputationSpecification[FPCFAnalysis]], + afterPhaseScheduling: (Project[URL], List[ComputationSpecification[FPCFAnalysis]]) => Unit = (_, _) => () ): TestContext = { try { - val p = FixtureProject.recreate { piKeyUnidueId => piKeyUnidueId != PropertyStoreKey.uniqueId } // to ensure that this project is not "polluted" - implicit val logContext: LogContext = p.logContext + val p = FixtureProject.recreate { piKeyUniqueId => piKeyUniqueId != PropertyStoreKey.uniqueId } // to ensure that this project is not "polluted" + init(p) + executeAnalysesForProject(p, analysisRunners, afterPhaseScheduling = afterPhaseScheduling(p, _)) + } catch { + case t: Throwable => + t.printStackTrace() + t.getSuppressed.foreach(e => e.printStackTrace()) + throw t; + } + } + + def executeAnalysesForProject( + project: Project[URL], + analysisRunners: Iterable[ComputationSpecification[FPCFAnalysis]], + afterPhaseScheduling: List[ComputationSpecification[FPCFAnalysis]] => Unit = _ => () + ): TestContext = { + try { + implicit val logContext: LogContext = project.logContext + PropertyStore.updateDebug(true) - p.getOrCreateProjectInformationKeyInitializationData( + project.getOrCreateProjectInformationKeyInitializationData( PropertyStoreKey, - (context: List[PropertyStoreContext[AnyRef]]) => { - /* - val ps = PKEParallelTasksPropertyStore.create( - new RecordAllPropertyStoreTracer, - context.iterator.map(_.asTuple).toMap - ) - */ - val ps = PKESequentialPropertyStore(context: _*) - ps - } + (context: List[PropertyStoreContext[AnyRef]]) => PKESequentialPropertyStore(context: _*) ) - val ps = p.get(PropertyStoreKey) + val ps = project.get(PropertyStoreKey) - val (_, csas) = p.get(FPCFAnalysesManagerKey).runAll(analysisRunners) - TestContext(p, ps, csas.collect { case (_, as) => as }) + val (_, csas) = project.get(FPCFAnalysesManagerKey).runAll(analysisRunners, afterPhaseScheduling(_)) + TestContext(project, ps, csas.collect { case (_, as) => as }) } catch { case t: Throwable => t.printStackTrace() diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/StringAnalysisTest.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/StringAnalysisTest.scala new file mode 100644 index 0000000000..8c04193921 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/StringAnalysisTest.scala @@ -0,0 +1,428 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package fpcf + +import java.net.URL +import java.util + +import com.typesafe.config.Config +import com.typesafe.config.ConfigValueFactory + +import org.opalj.ai.Domain +import org.opalj.ai.domain.RecordDefUse +import org.opalj.ai.domain.l1.DefaultDomainWithCFGAndDefUse +import org.opalj.ai.domain.l2.DefaultPerformInvocationsDomainWithCFGAndDefUse +import org.opalj.ai.fpcf.properties.AIDomainFactoryKey +import org.opalj.br.Annotation +import org.opalj.br.Annotations +import org.opalj.br.ElementValue +import org.opalj.br.ElementValuePair +import org.opalj.br.ElementValuePairs +import org.opalj.br.Method +import org.opalj.br.ObjectType +import org.opalj.br.StringValue +import org.opalj.br.analyses.DeclaredMethodsKey +import org.opalj.br.analyses.FieldAccessInformationKey +import org.opalj.br.analyses.Project +import org.opalj.br.fpcf.ContextProviderKey +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.PropertyStoreKey +import org.opalj.br.fpcf.properties.string.StringConstancyProperty +import org.opalj.fpcf.properties.string.Constant +import org.opalj.fpcf.properties.string.Constants +import org.opalj.fpcf.properties.string.DomainLevel +import org.opalj.fpcf.properties.string.Dynamic +import org.opalj.fpcf.properties.string.Dynamics +import org.opalj.fpcf.properties.string.Failure +import org.opalj.fpcf.properties.string.Failures +import org.opalj.fpcf.properties.string.Invalid +import org.opalj.fpcf.properties.string.Invalids +import org.opalj.fpcf.properties.string.Level +import org.opalj.fpcf.properties.string.PartiallyConstant +import org.opalj.fpcf.properties.string.PartiallyConstants +import org.opalj.fpcf.properties.string.SoundnessMode +import org.opalj.log.OPALLogger +import org.opalj.tac.EagerDetachedTACAIKey +import org.opalj.tac.PV +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.V +import org.opalj.tac.VirtualMethodCall +import org.opalj.tac.cg.CallGraphKey +import org.opalj.tac.cg.RTACallGraphKey +import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis +import org.opalj.tac.fpcf.analyses.string.StringAnalysis +import org.opalj.tac.fpcf.analyses.string.StringAnalysisConfig +import org.opalj.tac.fpcf.analyses.string.VariableContext +import org.opalj.tac.fpcf.analyses.string.flowanalysis.MethodStringFlowAnalysis +import org.opalj.tac.fpcf.analyses.string.l0.LazyL0StringAnalysis +import org.opalj.tac.fpcf.analyses.string.l1.LazyL1StringAnalysis +import org.opalj.tac.fpcf.analyses.string.l2.LazyL2StringAnalysis +import org.opalj.tac.fpcf.analyses.string.l3.LazyL3StringAnalysis +import org.opalj.tac.fpcf.analyses.systemproperties.TriggeredSystemPropertiesAnalysisScheduler + +// IMPROVE the test runner structure is far from optimal and could be reduced down to a simple test matrix. This however +// would require generating the wrapping "describes" and tag the tests using e.g. "asTagged" to be able to filter them +// during runs. +sealed abstract class StringAnalysisTest extends PropertiesTest { + + // The name of the method from which to extract PUVars to analyze. + val nameTestMethod: String = "analyzeString" + + final val callGraphKey: CallGraphKey = RTACallGraphKey + + def level: Level + def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] + def domainLevel: DomainLevel + def soundnessMode: SoundnessMode + + override def createConfig(): Config = { + val highSoundness = soundnessMode match { + case SoundnessMode.HIGH => true + case SoundnessMode.LOW => false + } + + super.createConfig() + .withValue(StringAnalysisConfig.HighSoundnessConfigKey, ConfigValueFactory.fromAnyRef(highSoundness)) + .withValue(StringAnalysis.DepthThresholdConfigKey, ConfigValueFactory.fromAnyRef(10)) + .withValue( + MethodStringFlowAnalysis.ExcludedPackagesConfigKey, + ConfigValueFactory.fromIterable(new util.ArrayList[String]()) + ) + } + + override def fixtureProjectPackage: List[String] = List("org/opalj/fpcf/fixtures/string") + + override def init(p: Project[URL]): Unit = { + val domain = domainLevel match { + case DomainLevel.L1 => classOf[DefaultDomainWithCFGAndDefUse[_]] + case DomainLevel.L2 => classOf[DefaultPerformInvocationsDomainWithCFGAndDefUse[_]] + case _ => throw new IllegalArgumentException(s"Invalid domain level for test definition: $domainLevel") + } + p.updateProjectInformationKeyInitializationData(AIDomainFactoryKey) { + case None => Set(domain) + case Some(requirements) => requirements + domain + } + p.updateProjectInformationKeyInitializationData(EagerDetachedTACAIKey) { + case None => m => domain.getConstructors.head.newInstance(p, m).asInstanceOf[Domain with RecordDefUse] + case Some(_) => m => domain.getConstructors.head.newInstance(p, m).asInstanceOf[Domain with RecordDefUse] + } + + val typeIterator = callGraphKey.getTypeIterator(p) + p.updateProjectInformationKeyInitializationData(ContextProviderKey) { _ => typeIterator } + } + + describe(s"using level=$level, domainLevel=$domainLevel, soundness=$soundnessMode") { + describe(s"the string analysis is started") { + var entities = Iterable.empty[(VariableContext, Method)] + val project = FixtureProject.recreate { piKeyUniqueId => piKeyUniqueId != PropertyStoreKey.uniqueId } + init(project) + + val as = executeAnalysesForProject( + project, + callGraphKey.allCallGraphAnalyses(project) ++ analyses, + currentPhaseAnalyses => { + if (currentPhaseAnalyses.exists(_.derives.exists(_.pk == StringConstancyProperty))) { + val ps = project.get(PropertyStoreKey) + entities = determineEntitiesToAnalyze(project) + entities.foreach(entity => ps.force(entity._1, StringConstancyProperty.key)) + } + } + ) + + as.propertyStore.waitOnPhaseCompletion() + as.propertyStore.shutdown() + + validateProperties(as, determineEAS(entities, project), Set("StringConstancy")) + } + } + + def determineEntitiesToAnalyze(project: Project[URL]): Iterable[(VariableContext, Method)] = { + val tacProvider = project.get(EagerDetachedTACAIKey) + val declaredMethods = project.get(DeclaredMethodsKey) + val contextProvider = project.get(ContextProviderKey) + project.allMethods + .filter(_.runtimeInvisibleAnnotations.nonEmpty) + .foldLeft(Seq.empty[(VariableContext, Method)]) { (entities, m) => + entities ++ extractPUVars(tacProvider(m)).map(e => + (VariableContext(e._1, e._2, contextProvider.newContext(declaredMethods(m))), m) + ) + } + } + + /** + * Extracts [[org.opalj.br.PUVar]]s from a set of statements. The locations of the [[org.opalj.br.PUVar]]s are + * identified by the argument to the very first call to [[nameTestMethod]]. + * + * @return Returns the arguments of the [[nameTestMethod]] as a PUVars list in the order in which they occurred. + */ + def extractPUVars(tac: TACode[TACMethodParameter, V]): List[(Int, PV)] = { + tac.cfg.code.instructions.filter { + case VirtualMethodCall(_, _, _, name, _, _, _) => name == nameTestMethod + case _ => false + }.map { call => (call.pc, call.asVirtualMethodCall.params.head.asVar.toPersistentForm(tac.stmts)) }.toList + } + + def determineEAS( + entities: Iterable[(VariableContext, Method)], + project: Project[URL] + ): Iterable[(VariableContext, String => String, List[Annotation])] = { + val m2e = entities.groupBy(_._2).iterator.map(e => + e._1 -> (e._1, e._2.map(k => k._1)) + ).toMap + + var detectedMissingAnnotations = false + // As entity, we need not the method but a tuple (PUVar, Method), thus this transformation + val eas = methodsWithAnnotations(project).filter(am => m2e.contains(am._1)).flatMap { am => + val annotationsByIndex = getCheckableAnnotationsByIndex(project, am._3) + m2e(am._1)._2.zipWithIndex.map { + case (vc, index) => + // Ensure every test that is annotated for at least one configuration is annotated for all + // configurations so we do not miss tests when adding new levels. + if (annotationsByIndex(index).isEmpty) { + OPALLogger.error( + "string analysis test setup", + s"Could not find annotations to check for #$index of ${am._1.toJava.substring(24)}" + )(project.logContext) + detectedMissingAnnotations = true + } + + Tuple3( + vc, + { s: String => s"${am._2(s)} (#$index)" }, + annotationsByIndex(index).toList + ) + } + } + + if (detectedMissingAnnotations) { + throw new IllegalStateException("Detected missing tests for this configuration!") + } + + eas + } + + private def getCheckableAnnotationsByIndex(project: Project[URL], as: Annotations): Map[Int, Annotations] = { + def mapFailure(failureEvp: ElementValuePairs): Annotation = { + if (soundnessMode == SoundnessMode.HIGH) + new Annotation( + ObjectType(classOf[Dynamic].getName.replace(".", "/")), + failureEvp.appended(ElementValuePair("value", StringValue(".*"))) + ) + else + new Annotation( + ObjectType(classOf[Invalid].getName.replace(".", "/")), + failureEvp + ) + } + + as.flatMap { + case a @ Annotation(annotationType, evp) + if annotationType.toJavaClass == classOf[Constant] + || annotationType.toJavaClass == classOf[PartiallyConstant] + || annotationType.toJavaClass == classOf[Dynamic] + || annotationType.toJavaClass == classOf[Invalid] => + Seq((evp.head.value.asIntValue.value, a)) + + case Annotation(annotationType, evp) if annotationType.toJavaClass == classOf[Failure] => + Seq((evp.head.value.asIntValue.value, mapFailure(evp))) + + case Annotation(annotationType, evp) + if annotationType.toJavaClass == classOf[Constants] + || annotationType.toJavaClass == classOf[PartiallyConstants] + || annotationType.toJavaClass == classOf[Dynamics] + || annotationType.toJavaClass == classOf[Invalids] => + evp.head.value.asArrayValue.values.toSeq.map { av => + val annotation = av.asAnnotationValue.annotation + (annotation.elementValuePairs.head.value.asIntValue.value, annotation) + } + + case Annotation(annotationType, evp) if annotationType.toJavaClass == classOf[Failures] => + evp.head.value.asArrayValue.values.toSeq.map { av => + val annotation = av.asAnnotationValue.annotation + (annotation.elementValuePairs.head.value.asIntValue.value, mapFailure(annotation.elementValuePairs)) + } + + case _ => + Seq.empty + }.groupBy(_._1).map { kv => + val annotations = kv._2.map(_._2) + .filter(fulfillsDomainLevel(project, _, domainLevel)) + .filter(fulfillsSoundness(project, _, soundnessMode)) + + val matchingCurrentLevel = annotations.filter(fulfillsLevel(project, _, level)) + if (matchingCurrentLevel.isEmpty) { + (kv._1, annotations.filter(fulfillsLevel(project, _, Level.TRUTH))) + } else { + (kv._1, matchingCurrentLevel) + } + } + } + + private def fulfillsLevel(p: Project[URL], a: Annotation, l: Level): Boolean = { + getValue(p, a, "levels").asArrayValue.values.exists(v => Level.valueOf(v.asEnumValue.constName) == l) + } + + private def fulfillsDomainLevel(p: Project[URL], a: Annotation, dl: DomainLevel): Boolean = { + getValue(p, a, "domains").asArrayValue.values.exists(v => DomainLevel.valueOf(v.asEnumValue.constName) == dl) + } + + private def fulfillsSoundness(p: Project[URL], a: Annotation, soundness: SoundnessMode): Boolean = { + getValue(p, a, "soundness").asArrayValue.values.exists { v => + SoundnessMode.valueOf(v.asEnumValue.constName) == soundness + } + } + + private def getValue(p: Project[URL], a: Annotation, name: String): ElementValue = { + a.elementValuePairs.collectFirst { + case ElementValuePair(`name`, value) => value + }.orElse { + // get default value ... + p.classFile(a.annotationType.asObjectType).get.findMethod(name).head.annotationDefault + }.get + } +} + +/** + * Tests whether the [[org.opalj.tac.fpcf.analyses.string.l0.LazyL0StringAnalysis]] works correctly with + * respect to some well-defined tests. + * + * @author Maximilian Rüsch + */ +sealed abstract class L0StringAnalysisTest extends StringAnalysisTest { + + override final def level = Level.L0 + + override final def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] = + LazyL0StringAnalysis.allRequiredAnalyses +} + +class L0StringAnalysisWithL1DefaultDomainTest extends L0StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L1 + override def soundnessMode: SoundnessMode = SoundnessMode.LOW +} + +class L0StringAnalysisWithL2DefaultDomainTest extends L0StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L2 + override def soundnessMode: SoundnessMode = SoundnessMode.LOW +} + +class HighSoundnessL0StringAnalysisWithL2DefaultDomainTest extends L0StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L2 + override def soundnessMode: SoundnessMode = SoundnessMode.HIGH +} + +/** + * Tests whether the [[org.opalj.tac.fpcf.analyses.string.l1.LazyL1StringAnalysis]] works correctly with + * respect to some well-defined tests. + * + * @author Maximilian Rüsch + */ +sealed abstract class L1StringAnalysisTest extends StringAnalysisTest { + + override final def level = Level.L1 + + override final def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] = { + LazyL1StringAnalysis.allRequiredAnalyses :+ + TriggeredSystemPropertiesAnalysisScheduler + } +} + +class L1StringAnalysisWithL1DefaultDomainTest extends L1StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L1 + override def soundnessMode: SoundnessMode = SoundnessMode.LOW +} + +class L1StringAnalysisWithL2DefaultDomainTest extends L1StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L2 + override def soundnessMode: SoundnessMode = SoundnessMode.LOW +} + +class HighSoundnessL1StringAnalysisWithL2DefaultDomainTest extends L1StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L2 + override def soundnessMode: SoundnessMode = SoundnessMode.HIGH +} + +/** + * Tests whether the [[org.opalj.tac.fpcf.analyses.string.l2.LazyL2StringAnalysis]] works correctly with + * respect to some well-defined tests. + * + * @author Maximilian Rüsch + */ +sealed abstract class L2StringAnalysisTest extends StringAnalysisTest { + + override def level = Level.L2 + + override final def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] = { + LazyL2StringAnalysis.allRequiredAnalyses :+ + TriggeredSystemPropertiesAnalysisScheduler + } +} + +class L2StringAnalysisWithL1DefaultDomainTest extends L2StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L1 + override def soundnessMode: SoundnessMode = SoundnessMode.LOW +} + +class L2StringAnalysisWithL2DefaultDomainTest extends L2StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L2 + override def soundnessMode: SoundnessMode = SoundnessMode.LOW +} + +class HighSoundnessL2StringAnalysisWithL2DefaultDomainTest extends L2StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L2 + override def soundnessMode: SoundnessMode = SoundnessMode.HIGH +} + +/** + * Tests whether the [[org.opalj.tac.fpcf.analyses.string.l3.LazyL3StringAnalysis]] works correctly with + * respect to some well-defined tests. + * + * @author Maximilian Rüsch + */ +sealed abstract class L3StringAnalysisTest extends StringAnalysisTest { + + override def level = Level.L3 + + override final def analyses: Iterable[ComputationSpecification[FPCFAnalysis]] = { + LazyL3StringAnalysis.allRequiredAnalyses :+ + EagerFieldAccessInformationAnalysis :+ + TriggeredSystemPropertiesAnalysisScheduler + } + + override def init(p: Project[URL]): Unit = { + super.init(p) + + p.updateProjectInformationKeyInitializationData(FieldAccessInformationKey) { + case None => Seq(EagerFieldAccessInformationAnalysis) + case Some(requirements) => requirements :+ EagerFieldAccessInformationAnalysis + } + } +} + +class L3StringAnalysisWithL1DefaultDomainTest extends L3StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L1 + override def soundnessMode: SoundnessMode = SoundnessMode.LOW +} + +class L3StringAnalysisWithL2DefaultDomainTest extends L3StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L2 + override def soundnessMode: SoundnessMode = SoundnessMode.LOW +} + +class HighSoundnessL3StringAnalysisWithL2DefaultDomainTest extends L3StringAnalysisTest { + + override def domainLevel: DomainLevel = DomainLevel.L2 + override def soundnessMode: SoundnessMode = SoundnessMode.HIGH +} diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string/StringMatcher.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string/StringMatcher.scala new file mode 100644 index 0000000000..c175662979 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/properties/string/StringMatcher.scala @@ -0,0 +1,85 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package fpcf +package properties +package string + +import java.util.regex.Pattern +import scala.util.Try + +import org.opalj.br.AnnotationLike +import org.opalj.br.ObjectType +import org.opalj.br.analyses.Project +import org.opalj.br.fpcf.properties.string.StringConstancyLevel +import org.opalj.br.fpcf.properties.string.StringConstancyProperty + +/** + * @see [[StringAnalysisTest]] + * @see [[org.opalj.fpcf.fixtures.string.SimpleStringOps]] + * @author Maximilian Rüsch + */ +sealed trait StringMatcher extends AbstractPropertyMatcher { + + protected def getActualValues: Property => Option[(String, String)] = { + case prop: StringConstancyProperty => + val tree = prop.tree.simplify.sorted + if (tree.isInvalid) { + None + } else { + Some((tree.constancyLevel.toString.toLowerCase, tree.toRegex)) + } + case p => throw new IllegalArgumentException(s"Tried to extract values from non string property: $p") + } +} + +sealed abstract class ConstancyStringMatcher(val constancyLevel: StringConstancyLevel) extends StringMatcher { + + override def validateProperty( + p: Project[_], + as: Set[ObjectType], + entity: Any, + a: AnnotationLike, + properties: Iterable[Property] + ): Option[String] = { + val expectedConstancy = constancyLevel.toString.toLowerCase + val expectedStrings = a.elementValuePairs.find(_.name == "value").get.value.asStringValue.value + val actualValuesOpt = getActualValues(properties.head) + if (actualValuesOpt.isEmpty) { + Some(s"Level: $expectedConstancy, Strings: $expectedStrings") + } else { + val (actLevel, actString) = actualValuesOpt.get + if (expectedConstancy != actLevel || expectedStrings != actString) { + Some(s"Level: $expectedConstancy, Strings: $expectedStrings") + } else { + val patternTry = Try(Pattern.compile(actString)) + if (patternTry.isFailure) { + Some(s"Same string, but it does not compile to a regex: ${patternTry.failed.get}") + } else { + None + } + } + } + } +} + +class ConstantStringMatcher extends ConstancyStringMatcher(StringConstancyLevel.Constant) +class PartiallyConstantStringMatcher extends ConstancyStringMatcher(StringConstancyLevel.PartiallyConstant) +class DynamicStringMatcher extends ConstancyStringMatcher(StringConstancyLevel.Dynamic) + +class InvalidStringMatcher extends StringMatcher { + + override def validateProperty( + p: Project[_], + as: Set[ObjectType], + entity: Any, + a: AnnotationLike, + properties: Iterable[Property] + ): Option[String] = { + val actualValuesOpt = getActualValues(properties.head) + if (actualValuesOpt.isDefined) { + Some(s"Invalid flow - No strings determined!") + } else { + None + } + } +} diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/SystemProperties.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/SystemProperties.scala index d6a0db5bd9..d8ff07860b 100644 --- a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/SystemProperties.scala +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/SystemProperties.scala @@ -4,6 +4,7 @@ package br package fpcf package properties +import org.opalj.br.fpcf.properties.string.StringTreeNode import org.opalj.fpcf.Entity import org.opalj.fpcf.FallbackReason import org.opalj.fpcf.Property @@ -13,20 +14,35 @@ import org.opalj.fpcf.PropertyMetaInformation import org.opalj.fpcf.PropertyStore /** - * TODO Documentation + * Holds the possible values that a [[java.util.Properties]] can take on, e.g. by analyzing the parameters given to + * calls like [[java.util.Properties.setProperty]] that are found in reachable methods. + *

+ * Currently, values are not distinguished by the keys they are set for since the key parameters may also take on any + * value conforming to their string tree, which can be infinitely many (see [[StringTreeNode]]). + *

+ * All existing analyses do not distinguish between the system-wide properties (set through [[System.setProperty]] or + * similar) and all other [[java.util.Properties]] instances. * - * @author Florian Kuebler + * @author Maximilian Rüsch */ sealed trait SystemPropertiesPropertyMetaInformation extends PropertyMetaInformation { + type Self = SystemProperties } -class SystemProperties(val properties: Map[String, Set[String]]) +case class SystemProperties(values: Set[StringTreeNode]) extends Property with SystemPropertiesPropertyMetaInformation { + + def mergeWith(other: SystemProperties): SystemProperties = { + if (values == other.values) this + else SystemProperties(values ++ other.values) + } + final def key: PropertyKey[SystemProperties] = SystemProperties.key } object SystemProperties extends SystemPropertiesPropertyMetaInformation { + final val Name = "opalj.SystemProperties" final val key: PropertyKey[SystemProperties] = { @@ -35,7 +51,7 @@ object SystemProperties extends SystemPropertiesPropertyMetaInformation { (_: PropertyStore, reason: FallbackReason, _: Entity) => reason match { case PropertyIsNotDerivedByPreviouslyExecutedAnalysis => - new SystemProperties(Map.empty) + new SystemProperties(Set.empty) case _ => throw new IllegalStateException(s"analysis required for property: $Name") } diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyLevel.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyLevel.scala new file mode 100644 index 0000000000..984d83bacf --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyLevel.scala @@ -0,0 +1,84 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package br +package fpcf +package properties +package string + +/** + * Documents the string constancy that a string tree consisting of [[StringTreeNode]] has, meaning a summary whether the + * string tree in question is invalid, has at most constant values, has at most constant values concatenated with + * dynamic values or also contains un-concatenated dynamic values. The companion object also defines useful combination + * functions for instances of this trait. + * + * @author Maximilian Rüsch + */ +sealed trait StringConstancyLevel + +object StringConstancyLevel { + + /** + * Indicates that a string has no value at a given read operation. + */ + case object Invalid extends StringConstancyLevel + + /** + * Indicates that a string has a constant value at a given read operation. + */ + case object Constant extends StringConstancyLevel + + /** + * Indicates that a string is partially constant (has a constant and a dynamic part) at some read operation. + */ + case object PartiallyConstant extends StringConstancyLevel + + /** + * Indicates that a string at some read operations has an unpredictable value. + */ + case object Dynamic extends StringConstancyLevel + + /** + * The more general StringConstancyLevel of the two given levels, i.e. the one that allows more possible + * values at the given read operation. + * + * @param level1 The first level. + * @param level2 The second level. + * @return Returns the more general level of both given inputs. + */ + def meet(level1: StringConstancyLevel, level2: StringConstancyLevel): StringConstancyLevel = { + if (level1 == Dynamic || level2 == Dynamic) { + Dynamic + } else if (level1 == PartiallyConstant || level2 == PartiallyConstant) { + PartiallyConstant + } else if (level1 == Constant || level2 == Constant) { + Constant + } else { + Invalid + } + } + + /** + * Returns the StringConstancyLevel of a concatenation of two values. + *

+ * + * @param level1 The first level. + * @param level2 The second level. + * @return Returns the level for a concatenation. + */ + def determineForConcat(level1: StringConstancyLevel, level2: StringConstancyLevel): StringConstancyLevel = { + if (level1 == Invalid || level2 == Invalid) { + Invalid + } else if (level1 == PartiallyConstant || level2 == PartiallyConstant) { + PartiallyConstant + } else if ((level1 == Constant && level2 == Dynamic) || (level1 == Dynamic && level2 == Constant)) { + PartiallyConstant + } else { + level1 + } + } +} diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyProperty.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyProperty.scala new file mode 100644 index 0000000000..245b420310 --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringConstancyProperty.scala @@ -0,0 +1,77 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package br +package fpcf +package properties +package string + +import org.opalj.fpcf.Entity +import org.opalj.fpcf.FallbackReason +import org.opalj.fpcf.Property +import org.opalj.fpcf.PropertyIsNotDerivedByPreviouslyExecutedAnalysis +import org.opalj.fpcf.PropertyKey +import org.opalj.fpcf.PropertyMetaInformation +import org.opalj.fpcf.PropertyStore + +/** + * Wrapper property around [[StringTreeNode]] to allow it to be stored in the [[PropertyStore]]. + * + * @author Maximilian Rüsch + */ +sealed trait StringConstancyPropertyMetaInformation extends PropertyMetaInformation { + final type Self = StringConstancyProperty +} + +class StringConstancyProperty( + val tree: StringTreeNode +) extends Property with StringConstancyPropertyMetaInformation { + + final def key: PropertyKey[StringConstancyProperty] = StringConstancyProperty.key + + override def toString: String = { + val level = tree.constancyLevel + val strings = if (level == StringConstancyLevel.Invalid) { + "No possible strings - Invalid Flow" + } else tree.sorted.toRegex + + s"Level: ${level.toString.toLowerCase}, Possible Strings: $strings" + } + + override def hashCode(): Int = tree.hashCode() + + override def equals(o: Any): Boolean = o match { + case scp: StringConstancyProperty => tree.equals(scp.tree) + case _ => false + } +} + +object StringConstancyProperty extends Property with StringConstancyPropertyMetaInformation { + + final val PropertyKeyName = "opalj.StringConstancy" + + final val key: PropertyKey[StringConstancyProperty] = { + PropertyKey.create( + PropertyKeyName, + (_: PropertyStore, reason: FallbackReason, _: Entity) => { + reason match { + case PropertyIsNotDerivedByPreviouslyExecutedAnalysis => + lb + case _ => + throw new IllegalStateException(s"Analysis required for property: $PropertyKeyName") + } + } + ) + } + + def apply(tree: StringTreeNode): StringConstancyProperty = new StringConstancyProperty(tree) + + /** + * @return The lower bound from a lattice-point of view. + */ + def lb: StringConstancyProperty = StringConstancyProperty(StringTreeNode.lb) + + /** + * @return The upper bound from a lattice-point of view. + */ + def ub: StringConstancyProperty = StringConstancyProperty(StringTreeNode.ub) +} diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringTreeNode.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringTreeNode.scala new file mode 100644 index 0000000000..2467f04cc3 --- /dev/null +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/string/StringTreeNode.scala @@ -0,0 +1,490 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package br +package fpcf +package properties +package string + +import scala.util.Try +import scala.util.matching.Regex + +/** + * A single node that can be nested to create string trees that represent a set of possible string values. Its canonical + * reduction is a regex of all possible strings. + * + * @note This trait and all its implementations should be kept immutable to allow certain values to be cached. + * @see [[CachedHashCode]] [[CachedSimplifyNode]] + * + * @author Maximilian Rüsch + */ +sealed trait StringTreeNode { + + val children: Seq[StringTreeNode] + + /** + * The depth of the string tree measured by the count of nodes on the longest path from the root to a leaf. + */ + lazy val depth: Int = children.map(_.depth).maxOption.getOrElse(0) + 1 + + /** + * Replaces string tree nodes at the target depth if they have children. In case a [[SimpleStringTreeNode]] is given + * as a second parameter, this effectively limits the string tree to the given target depth. + * + * @param targetDepth The depth at which nodes should be replaced if they have children. + * @param replacement The replacement to set for nodes at the target depth if they have children. + * @return The modified tree if the target depth is smaller than the current depth or the same instance if it is not. + */ + final def replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = { + if (targetDepth >= depth) + this + else + _replaceAtDepth(targetDepth, replacement) + } + protected def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode + + /** + * @return The string tree sorted with a stable ordering over its canonical reduction. + */ + def sorted: StringTreeNode + + private var _regex: Option[String] = None + + /** + * @return The canonical reduction of the string tree, i.e. a regex representing the same set of string values as + * the tree itself. + */ + final def toRegex: String = { + if (_regex.isEmpty) { + _regex = Some(_toRegex) + } + + _regex.get + } + protected def _toRegex: String + + /** + * Simplifies the string tree by e.g. flattening nested [[StringTreeOr]] instances. + * + * @return The simplified string tree or the same instance if nothing could be simplified. + * + * @see [[CachedSimplifyNode]] + */ + def simplify: StringTreeNode + + /** + * @return The constancy level of the string tree. + * + * @see [[StringConstancyLevel]] + */ + def constancyLevel: StringConstancyLevel + + /** + * The indices of any method parameter references using [[StringTreeParameter]] within the string tree. + */ + lazy val parameterIndices: Set[Int] = children.flatMap(_.parameterIndices).toSet + + /** + * Replaces all [[StringTreeParameter]] instances in the string tree that represent a parameter index defined in the + * given map with the replacement value for that index. Keeps [[StringTreeParameter]] instances whose their index is + * not defined in the map. + * + * @param parameters A map from parameter indices to replacement values + * @return The modified string tree if something could be replaced or the same instance otherwise. + */ + final def replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = { + if (parameters.isEmpty || + parameterIndices.isEmpty || + parameters.keySet.intersect(parameterIndices).isEmpty + ) + this + else + _replaceParameters(parameters) + } + protected def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode + + /** + * @return True if this string tree node represents an empty string, false otherwise. + */ + def isEmpty: Boolean = false + + /** + * @return True if this string tree node represents no string, false otherwise. + */ + def isInvalid: Boolean = false +} + +object StringTreeNode { + + def lb: StringTreeNode = StringTreeDynamicString + def ub: StringTreeNode = StringTreeInvalidElement +} + +sealed trait CachedSimplifyNode extends StringTreeNode { + + private var _simplified = false + final def simplify: StringTreeNode = { + if (_simplified) { + this + } else { + _simplify match { + case cr: CachedSimplifyNode => + cr._simplified = true + cr + case r => r + } + } + } + + protected def _simplify: StringTreeNode +} + +sealed trait CachedHashCode extends Product { + + private lazy val _hashCode = scala.util.hashing.MurmurHash3.productHash(this) + override def hashCode(): Int = _hashCode + override def canEqual(obj: Any): Boolean = obj.hashCode() == _hashCode +} + +/** + * Represents the concatenation of all its children. + */ +case class StringTreeConcat(override val children: Seq[StringTreeNode]) extends CachedSimplifyNode with CachedHashCode { + + override protected def _toRegex: String = { + children.size match { + case 0 => throw new IllegalStateException("Tried to convert StringTreeConcat with no children to a regex!") + case 1 => children.head.toRegex + case _ => s"${children.map(_.toRegex).reduceLeft((o, n) => s"$o$n")}" + } + } + + override def sorted: StringTreeNode = StringTreeConcat(children.map(_.sorted)) + + override def _simplify: StringTreeNode = { + val nonEmptyChildren = children.map(_.simplify).filterNot(_.isEmpty) + if (nonEmptyChildren.exists(_.isInvalid)) { + StringTreeInvalidElement + } else { + nonEmptyChildren.size match { + case 0 => StringTreeEmptyConst + case 1 => nonEmptyChildren.head + case _ => + var newChildren = Seq.empty[StringTreeNode] + nonEmptyChildren.foreach { + case concatChild: StringTreeConcat => newChildren :++= concatChild.children + case child => newChildren :+= child + } + StringTreeConcat(newChildren) + } + } + } + + override def constancyLevel: StringConstancyLevel = + children.map(_.constancyLevel).reduceLeft(StringConstancyLevel.determineForConcat) + + def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = { + val childrenWithChange = children.map { c => + val nc = c.replaceParameters(parameters) + (nc, c ne nc) + } + + if (childrenWithChange.exists(_._2)) { + StringTreeConcat(childrenWithChange.map(_._1)) + } else { + this + } + } + + def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = { + if (targetDepth == 1) + replacement + else + StringTreeConcat(children.map(_.replaceAtDepth(targetDepth - 1, replacement))) + } +} + +object StringTreeConcat { + def fromNodes(children: StringTreeNode*): StringTreeNode = { + if (children.isEmpty || children.exists(_.isInvalid)) { + StringTreeInvalidElement + } else if (children.forall(_.isEmpty)) { + StringTreeEmptyConst + } else { + new StringTreeConcat(children.filterNot(_.isEmpty)) + } + } +} + +/** + * Represents the free choice between all its children. + */ +trait StringTreeOr extends CachedSimplifyNode with CachedHashCode { + + protected val _children: Iterable[StringTreeNode] + + override final lazy val children: Seq[StringTreeNode] = _children.toSeq + + override protected def _toRegex: String = { + children.size match { + case 0 => throw new IllegalStateException("Tried to convert StringTreeOr with no children to a regex!") + case 1 => children.head.toRegex + case _ => s"(${children.map(_.toRegex).reduceLeft((o, n) => s"$o|$n")})" + } + } + + override def constancyLevel: StringConstancyLevel = + _children.map(_.constancyLevel).reduceLeft(StringConstancyLevel.meet) + + override lazy val parameterIndices: Set[Int] = _children.flatMap(_.parameterIndices).toSet +} + +object StringTreeOr { + + def apply(children: Seq[StringTreeNode]): StringTreeNode = apply(children.toSet) + + def apply(children: Set[StringTreeNode]): StringTreeNode = { + if (children.isEmpty) { + StringTreeInvalidElement + } else if (children.size == 1) { + children.head + } else { + new SetBasedStringTreeOr(children) + } + } + + def fromNodes(children: StringTreeNode*): StringTreeNode = SetBasedStringTreeOr.createWithSimplify(children.toSet) +} + +/** + * @inheritdoc + * + * Based on a [[Seq]] for children storage. To be used if the order of children is important for e.g. reduction to a + * regex and subsequent comparison to another string tree. + */ +private case class SeqBasedStringTreeOr(override val _children: Seq[StringTreeNode]) extends StringTreeOr { + + override def sorted: StringTreeNode = SeqBasedStringTreeOr(children.map(_.sorted).sortBy(_.toRegex)) + + override def _simplify: StringTreeNode = { + val validChildren = _children.map(_.simplify).filterNot(_.isInvalid) + validChildren.size match { + case 0 => StringTreeInvalidElement + case 1 => validChildren.head + case _ => + val newChildren = validChildren.flatMap { + case orChild: StringTreeOr => orChild.children + case child => Set(child) + } + val distinctNewChildren = newChildren.distinct + distinctNewChildren.size match { + case 1 => distinctNewChildren.head + case _ => SeqBasedStringTreeOr(distinctNewChildren) + } + } + } + + def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = { + val childrenWithChange = _children.map { c => + val nc = c.replaceParameters(parameters) + (nc, c ne nc) + } + + if (childrenWithChange.exists(_._2)) { + SeqBasedStringTreeOr(childrenWithChange.map(_._1)) + } else { + this + } + } + + def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = { + if (targetDepth == 1) + replacement + else + SeqBasedStringTreeOr(children.map(_.replaceAtDepth(targetDepth - 1, replacement))) + } +} + +object SeqBasedStringTreeOr { + + def apply(children: Seq[StringTreeNode]): StringTreeNode = { + if (children.isEmpty) { + StringTreeInvalidElement + } else if (children.size == 1) { + children.head + } else { + new SeqBasedStringTreeOr(children) + } + } +} + +/** + * @inheritdoc + * + * Based on a [[Set]] for children storage. To be used if the order of children is NOT important. + */ +case class SetBasedStringTreeOr(override val _children: Set[StringTreeNode]) extends StringTreeOr { + + override lazy val depth: Int = _children.map(_.depth).maxOption.getOrElse(0) + 1 + + override def sorted: StringTreeNode = SeqBasedStringTreeOr(children.map(_.sorted).sortBy(_.toRegex)) + + override def _simplify: StringTreeNode = SetBasedStringTreeOr._simplifySelf { + _children.map(_.simplify).filterNot(_.isInvalid) + } + + def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = { + val childrenWithChange = _children.map { c => + val nc = c.replaceParameters(parameters) + (nc, c ne nc) + } + + if (childrenWithChange.exists(_._2)) { + SetBasedStringTreeOr(childrenWithChange.map(_._1)) + } else { + this + } + } + + def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = { + if (targetDepth == 1) + replacement + else + SetBasedStringTreeOr(_children.map(_.replaceAtDepth(targetDepth - 1, replacement))) + } +} + +object SetBasedStringTreeOr { + + def apply(children: Set[StringTreeNode]): StringTreeNode = { + if (children.isEmpty) { + StringTreeInvalidElement + } else if (children.size == 1) { + children.head + } else { + new SetBasedStringTreeOr(children) + } + } + + def createWithSimplify(children: Set[StringTreeNode]): StringTreeNode = + _simplifySelf(children.filterNot(_.isInvalid)) + + private def _simplifySelf(_children: Set[StringTreeNode]): StringTreeNode = { + _children.size match { + case 0 => StringTreeInvalidElement + case 1 => _children.head + case _ => + val newChildrenBuilder = Set.newBuilder[StringTreeNode] + _children.foreach { + case setOrChild: SetBasedStringTreeOr => newChildrenBuilder.addAll(setOrChild._children) + case orChild: StringTreeOr => newChildrenBuilder.addAll(orChild.children) + case child => newChildrenBuilder.addOne(child) + } + val newChildren = newChildrenBuilder.result() + newChildren.size match { + case 1 => newChildren.head + case _ => SetBasedStringTreeOr(newChildren) + } + } + } +} + +/** + * Represents a string tree leaf, i.e. a node having no children and can thus return itself during sorting and + * simplification. + */ +sealed trait SimpleStringTreeNode extends StringTreeNode { + + override final val children: Seq[StringTreeNode] = Seq.empty + + override final def sorted: StringTreeNode = this + override final def simplify: StringTreeNode = this + + override def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = this + override def _replaceAtDepth(targetDepth: Int, replacement: StringTreeNode): StringTreeNode = { + if (targetDepth == 1) + replacement + else + replaceAtDepth(targetDepth - 1, replacement) + } +} + +case class StringTreeConst(string: String) extends SimpleStringTreeNode { + override protected def _toRegex: String = Regex.quoteReplacement(string).replaceAll("\\[", "\\\\[") + + override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Constant + + def isIntConst: Boolean = Try(string.toInt).isSuccess + + override def isEmpty: Boolean = string == "" +} + +object StringTreeEmptyConst extends StringTreeConst("") { + + override def isEmpty: Boolean = true +} + +/** + * A placeholder for a method parameter value. Should be replaced using [[replaceParameters]] before reducing the string + * tree to a regex. + * + * @param index The method parameter index that is being represented. + */ +case class StringTreeParameter(index: Int) extends SimpleStringTreeNode { + override protected def _toRegex: String = ".*" + + override lazy val parameterIndices: Set[Int] = Set(index) + + override def _replaceParameters(parameters: Map[Int, StringTreeNode]): StringTreeNode = + parameters.getOrElse(index, this) + + override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Dynamic +} + +object StringTreeParameter { + + def forParameterPC(paramPC: Int): StringTreeParameter = { + if (paramPC >= -1) { + throw new IllegalArgumentException(s"Invalid parameter pc given: $paramPC") + } + // Parameters start at PC -2 downwards + StringTreeParameter(Math.abs(paramPC + 2)) + } +} + +object StringTreeInvalidElement extends SimpleStringTreeNode { + override protected def _toRegex: String = throw new UnsupportedOperationException() + + override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Invalid + + override def isInvalid: Boolean = true +} + +object StringTreeNull extends SimpleStringTreeNode { + // IMPROVE Using this element nested in some other element might lead to unexpected results since it contains regex + // matching characters for the beginning and end of a string. + override protected def _toRegex: String = "^null$" + + override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Constant +} + +object StringTreeDynamicString extends SimpleStringTreeNode { + override protected def _toRegex: String = ".*" + + override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Dynamic +} + +object StringTreeDynamicInt extends SimpleStringTreeNode { + // IMPROVE Using this element nested in some other element might lead to unexpected results since it contains regex + // matching characters for the beginning and end of a string. + override protected def _toRegex: String = "^-?\\d+$" + + override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Dynamic +} + +object StringTreeDynamicFloat extends SimpleStringTreeNode { + // IMPROVE Using this element nested in some other element might lead to unexpected results since it contains regex + // matching characters for the beginning and end of a string. + override protected def _toRegex: String = "^-?\\d*\\.{0,1}\\d+$" + + override def constancyLevel: StringConstancyLevel = StringConstancyLevel.Dynamic +} diff --git a/OPAL/br/src/test/scala/org/opalj/br/string/StringConstancyLevelTests.scala b/OPAL/br/src/test/scala/org/opalj/br/string/StringConstancyLevelTests.scala new file mode 100644 index 0000000000..7bd9cb49f9 --- /dev/null +++ b/OPAL/br/src/test/scala/org/opalj/br/string/StringConstancyLevelTests.scala @@ -0,0 +1,73 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package br +package string + +import org.scalatest.funsuite.AnyFunSuite + +import org.opalj.br.fpcf.properties.string.StringConstancyLevel +import org.opalj.br.fpcf.properties.string.StringConstancyLevel.Constant +import org.opalj.br.fpcf.properties.string.StringConstancyLevel.Dynamic +import org.opalj.br.fpcf.properties.string.StringConstancyLevel.Invalid +import org.opalj.br.fpcf.properties.string.StringConstancyLevel.PartiallyConstant + +/** + * Tests for [[StringConstancyLevel]] methods. + * + * @author Maximilian Rüsch + */ +@org.junit.runner.RunWith(classOf[org.scalatestplus.junit.JUnitRunner]) +class StringConstancyLevelTests extends AnyFunSuite { + + test("tests that the more general string constancy level is computed correctly") { + // Trivial cases + assert(StringConstancyLevel.meet(Invalid, Invalid) == Invalid) + assert(StringConstancyLevel.meet(Constant, Constant) == Constant) + assert(StringConstancyLevel.meet(PartiallyConstant, PartiallyConstant) == PartiallyConstant) + assert(StringConstancyLevel.meet(Dynamic, Dynamic) == Dynamic) + + // <= Constant + assert(StringConstancyLevel.meet(Constant, Invalid) == Constant) + assert(StringConstancyLevel.meet(Invalid, Constant) == Constant) + + // <= PartiallyConstant + assert(StringConstancyLevel.meet(PartiallyConstant, Invalid) == PartiallyConstant) + assert(StringConstancyLevel.meet(Invalid, PartiallyConstant) == PartiallyConstant) + assert(StringConstancyLevel.meet(PartiallyConstant, Constant) == PartiallyConstant) + assert(StringConstancyLevel.meet(Constant, PartiallyConstant) == PartiallyConstant) + + // <= Dynamic + assert(StringConstancyLevel.meet(Invalid, Dynamic) == Dynamic) + assert(StringConstancyLevel.meet(Dynamic, Invalid) == Dynamic) + assert(StringConstancyLevel.meet(Constant, Dynamic) == Dynamic) + assert(StringConstancyLevel.meet(Dynamic, Constant) == Dynamic) + assert(StringConstancyLevel.meet(PartiallyConstant, Dynamic) == Dynamic) + assert(StringConstancyLevel.meet(Dynamic, PartiallyConstant) == Dynamic) + } + + test("tests that the string constancy level for concatenation of two levels is computed correctly") { + // Trivial cases + assert(StringConstancyLevel.determineForConcat(Invalid, Invalid) == Invalid) + assert(StringConstancyLevel.determineForConcat(Constant, Constant) == Constant) + assert(StringConstancyLevel.determineForConcat(PartiallyConstant, PartiallyConstant) == PartiallyConstant) + assert(StringConstancyLevel.determineForConcat(Dynamic, Dynamic) == Dynamic) + + // Invalid blocks everything + assert(StringConstancyLevel.determineForConcat(Constant, Invalid) == Invalid) + assert(StringConstancyLevel.determineForConcat(Invalid, Constant) == Invalid) + assert(StringConstancyLevel.determineForConcat(PartiallyConstant, Invalid) == Invalid) + assert(StringConstancyLevel.determineForConcat(Invalid, PartiallyConstant) == Invalid) + assert(StringConstancyLevel.determineForConcat(Invalid, Dynamic) == Invalid) + assert(StringConstancyLevel.determineForConcat(Dynamic, Invalid) == Invalid) + + // PartiallyConstant can be retained + assert(StringConstancyLevel.determineForConcat(PartiallyConstant, Constant) == PartiallyConstant) + assert(StringConstancyLevel.determineForConcat(Constant, PartiallyConstant) == PartiallyConstant) + assert(StringConstancyLevel.determineForConcat(PartiallyConstant, Dynamic) == PartiallyConstant) + assert(StringConstancyLevel.determineForConcat(Dynamic, PartiallyConstant) == PartiallyConstant) + + // PartiallyConstant can be constructed + assert(StringConstancyLevel.determineForConcat(Constant, Dynamic) == PartiallyConstant) + assert(StringConstancyLevel.determineForConcat(Dynamic, Constant) == PartiallyConstant) + } +} diff --git a/OPAL/tac/src/main/resources/reference.conf b/OPAL/tac/src/main/resources/reference.conf index b21ce227c9..3f4707d64f 100644 --- a/OPAL/tac/src/main/resources/reference.conf +++ b/OPAL/tac/src/main/resources/reference.conf @@ -112,9 +112,34 @@ org.opalj { description = "Produces points-to sets for the results of reflection methods like Class.getMethod.", eagerFactory = "org.opalj.tac.fpcf.analyses.pointsto.ReflectionAllocationsAnalysisScheduler" }, + "StringAnalysis" { + description = "Computes strings available on individual variables in the context of the method they are defined in.", + lazyFactory = "org.opalj.tac.fpcf.analyses.string.LazyStringAnalysis" + }, + "MethodStringFlowAnalysis" { + description = "Computes results of data flow analysis for string variables in the context of a method.", + lazyFactory = "org.opalj.tac.fpcf.analyses.string.flowanalysis.MethodStringFlowAnalysis" + }, + "L0StringFlowAnalysis" { + description = "Computes string flow functions on level L0 for individual statements in the context of a method to be used in data flow analysis.", + lazyFactory = "org.opalj.tac.fpcf.analyses.string.l0.LazyL0StringFlowAnalysis" + }, + "L1StringFlowAnalysis" { + description = "Computes string flow functions on level L1 for individual statements in the context of a method to be used in data flow analysis.", + lazyFactory = "org.opalj.tac.fpcf.analyses.string.l1.LazyL1StringFlowAnalysis" + }, + "L2StringFlowAnalysis" { + description = "Computes string flow functions on level L2 for individual statements in the context of a method to be used in data flow analysis.", + lazyFactory = "org.opalj.tac.fpcf.analyses.string.l2.LazyL2StringFlowAnalysis", + default = true + }, + "L3StringFlowAnalysis" { + description = "Computes string flow functions on level L3 for individual statements in the context of a method to be used in data flow analysis.", + lazyFactory = "org.opalj.tac.fpcf.analyses.string.l3.LazyL3StringFlowAnalysis" + }, "SystemPropertiesAnalysis" { - description = "Computes Strings available from system properties.", - triggeredFactory = "org.opalj.tac.fpcf.analyses.SystemPropertiesAnalysisScheduler" + description = "Computes strings available from all instances of the Properties class.", + triggeredFactory = "org.opalj.tac.fpcf.analyses.systemproperties.TriggeredSystemPropertiesAnalysisScheduler" } } }, @@ -1529,7 +1554,14 @@ org.opalj { mergeExceptions = true }, cg.reflection.ReflectionRelatedCallsAnalysis.highSoundness = "" // e.g. "all" or "class,method", - fieldaccess.reflection.ReflectionRelatedFieldAccessesAnalysis.highSoundness = false + fieldaccess.reflection.ReflectionRelatedFieldAccessesAnalysis.highSoundness = false, + string { + highSoundness = false, + StringAnalysis.depthThreshold = 10 + MethodStringFlowAnalysis.excludedPackages = [ + "sun.nio.cs" // Due to problems with the size of the static initialization in the charsets + ] + } } }, tac.cg { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/DUVar.scala b/OPAL/tac/src/main/scala/org/opalj/tac/DUVar.scala index 88bcddd54b..abf4b5de8e 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/DUVar.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/DUVar.scala @@ -10,6 +10,7 @@ import org.opalj.ai.pcOfMethodExternalException import org.opalj.br.ComputationalType import org.opalj.br.ComputationalTypeReturnAddress import org.opalj.br.PDUVar +import org.opalj.br.PDVar import org.opalj.br.PUVar import org.opalj.collection.immutable.IntTrieSet import org.opalj.value.ValueInformation @@ -187,7 +188,9 @@ class DVar[+Value <: ValueInformation /*org.opalj.ai.ValuesDomain#DomainValue*/ s"DVar(useSites=${useSites.mkString("{", ",", "}")},value=$value,origin=$origin)" } - override def toPersistentForm(implicit stmts: Array[Stmt[V]]): Nothing = throw new UnsupportedOperationException + override def toPersistentForm( + implicit stmts: Array[Stmt[V]] + ): PDVar[Value] = PDVar(value, usedBy.map(pcOfDefSite _)) } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/PDUWeb.scala b/OPAL/tac/src/main/scala/org/opalj/tac/PDUWeb.scala new file mode 100644 index 0000000000..12b638b9f8 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/PDUWeb.scala @@ -0,0 +1,38 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac + +import org.opalj.br.PDVar +import org.opalj.br.PUVar +import org.opalj.collection.immutable.IntTrieSet + +/** + * Identifies a variable inside a given fixed method. A methods webs can be constructed through the maximal unions of + * all intersecting DU-UD-chains of the method. + * + * @param defPCs The def PCs of the variable that is identified through this web. + * @param usePCs The use PCs of the variable that is identified through this web. + * + * @author Maximilian Rüsch + */ +case class PDUWeb( + defPCs: IntTrieSet, + usePCs: IntTrieSet +) { + def identifiesSameVarAs(other: PDUWeb): Boolean = other.defPCs.intersect(defPCs).nonEmpty + + def combine(other: PDUWeb): PDUWeb = PDUWeb(other.defPCs ++ defPCs, other.usePCs ++ usePCs) + + // Performance optimizations + private lazy val _hashCode = scala.util.hashing.MurmurHash3.productHash(this) + override def hashCode(): Int = _hashCode + override def equals(obj: Any): Boolean = obj.hashCode() == _hashCode && super.equals(obj) +} + +object PDUWeb { + + def apply(pc: Int, pv: PV): PDUWeb = pv match { + case pdVar: PDVar[_] => PDUWeb(IntTrieSet(pc), pdVar.usePCs) + case puVar: PUVar[_] => PDUWeb(puVar.defPCs, IntTrieSet(pc)) + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/SystemPropertiesAnalysisScheduler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/SystemPropertiesAnalysisScheduler.scala deleted file mode 100644 index e35ccd3c9e..0000000000 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/SystemPropertiesAnalysisScheduler.scala +++ /dev/null @@ -1,163 +0,0 @@ -/* BSD 2-Clause License - see OPAL/LICENSE for details. */ -package org.opalj -package tac -package fpcf -package analyses - -import org.opalj.br.Method -import org.opalj.br.ObjectType -import org.opalj.br.analyses.DeclaredMethodsKey -import org.opalj.br.analyses.ProjectInformationKeys -import org.opalj.br.analyses.SomeProject -import org.opalj.br.fpcf.BasicFPCFTriggeredAnalysisScheduler -import org.opalj.br.fpcf.properties.SystemProperties -import org.opalj.br.fpcf.properties.cg.Callers -import org.opalj.fpcf.EOptionP -import org.opalj.fpcf.EPK -import org.opalj.fpcf.EPS -import org.opalj.fpcf.InterimEP -import org.opalj.fpcf.InterimEUBP -import org.opalj.fpcf.InterimPartialResult -import org.opalj.fpcf.PartialResult -import org.opalj.fpcf.ProperPropertyComputationResult -import org.opalj.fpcf.PropertyBounds -import org.opalj.fpcf.PropertyKey -import org.opalj.fpcf.PropertyStore -import org.opalj.fpcf.Results -import org.opalj.fpcf.UBP -import org.opalj.tac.cg.TypeIteratorKey -import org.opalj.tac.fpcf.analyses.cg.ReachableMethodAnalysis -import org.opalj.tac.fpcf.properties.TACAI -import org.opalj.value.ValueInformation - -class SystemPropertiesAnalysisScheduler private[analyses] ( - final val project: SomeProject -) extends ReachableMethodAnalysis { - - def processMethod( - callContext: ContextType, - tacaiEP: EPS[Method, TACAI] - ): ProperPropertyComputationResult = { - assert(tacaiEP.hasUBP && tacaiEP.ub.tac.isDefined) - val stmts = tacaiEP.ub.tac.get.stmts - - var propertyMap: Map[String, Set[String]] = Map.empty - - for (stmt <- stmts) stmt match { - case VirtualFunctionCallStatement(call) - if (call.name == "setProperty" || call.name == "put") && classHierarchy.isSubtypeOf( - call.declaringClass, - ObjectType("java/util/Properties") - ) => - propertyMap = computeProperties(propertyMap, call.params, stmts) - case StaticMethodCall(_, ObjectType.System, _, "setProperty", _, params) => - propertyMap = computeProperties(propertyMap, params, stmts) - case _ => - } - - if (propertyMap.isEmpty) { - return Results() - } - - def update( - currentVal: EOptionP[SomeProject, SystemProperties] - ): Option[InterimEP[SomeProject, SystemProperties]] = currentVal match { - case UBP(ub) => - var oldProperties = ub.properties - val noNewProperty = propertyMap.forall { - case (key, values) => - oldProperties.contains(key) && { - val oldValues = oldProperties(key) - values.forall(oldValues.contains) - } - } - - if (noNewProperty) { - None - } else { - for ((key, values) <- propertyMap) { - val oldValues = oldProperties.getOrElse(key, Set.empty) - oldProperties = oldProperties.updated(key, oldValues ++ values) - } - Some(InterimEUBP(project, new SystemProperties(propertyMap))) - } - - case _: EPK[SomeProject, SystemProperties] => - Some(InterimEUBP(project, new SystemProperties(propertyMap))) - } - - if (tacaiEP.isFinal) { - PartialResult[SomeProject, SystemProperties]( - project, - SystemProperties.key, - update - ) - } else { - InterimPartialResult( - project, - SystemProperties.key, - update, - Set(tacaiEP), - continuationForTAC(callContext.method) - ) - } - } - - def computeProperties( - propertyMap: Map[String, Set[String]], - params: Seq[Expr[DUVar[ValueInformation]]], - stmts: Array[Stmt[DUVar[ValueInformation]]] - ): Map[String, Set[String]] = { - var res = propertyMap - - assert(params.size == 2) - val possibleKeys = getPossibleStrings(params.head, stmts) - val possibleValues = getPossibleStrings(params(1), stmts) - - for (key <- possibleKeys) { - val values = res.getOrElse(key, Set.empty) - res = res.updated(key, values ++ possibleValues) - } - - res - } - - def getPossibleStrings( - value: Expr[DUVar[ValueInformation]], - stmts: Array[Stmt[DUVar[ValueInformation]]] - ): Set[String] = { - value.asVar.definedBy filter { index => index >= 0 && stmts(index).asAssignment.expr.isStringConst } map { - stmts(_).asAssignment.expr.asStringConst.value - } - } - -} - -object SystemPropertiesAnalysisScheduler extends BasicFPCFTriggeredAnalysisScheduler { - - override def requiredProjectInformation: ProjectInformationKeys = - Seq(DeclaredMethodsKey, TypeIteratorKey) - - override def uses: Set[PropertyBounds] = Set( - PropertyBounds.ub(Callers), - PropertyBounds.ub(TACAI) - ) - - override def triggeredBy: PropertyKey[Callers] = Callers.key - - override def register( - p: SomeProject, - ps: PropertyStore, - unused: Null - ): SystemPropertiesAnalysisScheduler = { - val analysis = new SystemPropertiesAnalysisScheduler(p) - ps.registerTriggeredComputation(triggeredBy, analysis.analyze) - analysis - } - - override def derivesEagerly: Set[PropertyBounds] = Set.empty - - override def derivesCollaboratively: Set[PropertyBounds] = Set( - PropertyBounds.ub(SystemProperties) - ) -} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysis.scala new file mode 100644 index 0000000000..cc4b1ba544 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysis.scala @@ -0,0 +1,402 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string + +import scala.collection.mutable.ListBuffer + +import org.opalj.br.DeclaredMethod +import org.opalj.br.Method +import org.opalj.br.PDVar +import org.opalj.br.PUVar +import org.opalj.br.analyses.SomeProject +import org.opalj.br.fpcf.ContextProviderKey +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.analyses.ContextProvider +import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.cg.Callers +import org.opalj.br.fpcf.properties.cg.NoCallers +import org.opalj.br.fpcf.properties.string.StringConstancyProperty +import org.opalj.br.fpcf.properties.string.StringTreeConst +import org.opalj.br.fpcf.properties.string.StringTreeNode +import org.opalj.br.fpcf.properties.string.StringTreeOr +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.EPK +import org.opalj.fpcf.EPS +import org.opalj.fpcf.EUBP +import org.opalj.fpcf.InterimResult +import org.opalj.fpcf.ProperPropertyComputationResult +import org.opalj.fpcf.Result +import org.opalj.fpcf.SomeEPS +import org.opalj.fpcf.UBP +import org.opalj.log.Error +import org.opalj.log.Info +import org.opalj.log.OPALLogger.logOnce +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.fpcf.properties.string.MethodStringFlow + +/** + * Base trait for all string analyses that compute results for the FPCF [[StringConstancyProperty]]. + * + * @author Maximilian Rüsch + */ +trait StringAnalysis extends FPCFAnalysis with StringAnalysisConfig { + + private final val ConfigLogCategory = "analysis configuration - string analysis" + + /** + * @see [[StringAnalysis.DepthThresholdConfigKey]] + */ + protected val depthThreshold: Int = { + val depthThreshold = + try { + project.config.getInt(StringAnalysis.DepthThresholdConfigKey) + } catch { + case t: Throwable => + logOnce(Error(ConfigLogCategory, s"couldn't read: ${StringAnalysis.DepthThresholdConfigKey}", t)) + 10 + } + + logOnce(Info(ConfigLogCategory, "using depth threshold " + depthThreshold)) + depthThreshold + } +} + +object StringAnalysis { + + /** + * The string tree depth after which the string analysis does not continue and returns either the current tree + * or the tree limited to the depth threshold, depending on the soundness mode in use. + */ + final val DepthThresholdConfigKey = "org.opalj.fpcf.analyses.string.StringAnalysis.depthThreshold" +} + +/** + * Analyzes a given variable in context of its method in a context free manner, i.e. without trying to resolve method + * parameter references present in the string tree computed for the variable. + * + * @note This particular instance of the analysis also tries to resolve string constants without involving the + * [[MethodStringFlow]] FPCF property in an effort to maintain scalability. Once a variable is known to require + * flow-sensitive information, a [[MethodStringFlow]] property is requested from the property store and all + * subsequent computations are made simply in reaction to changes in the method string flow dependee. + * + * @see [[org.opalj.tac.fpcf.analyses.string.flowanalysis.MethodStringFlowAnalysis]], [[ContextStringAnalysis]] + * + * @author Maximilian Rüsch + */ +private[string] class ContextFreeStringAnalysis(override val project: SomeProject) extends StringAnalysis { + + def analyze(vd: VariableDefinition): ProperPropertyComputationResult = { + implicit val state: ContextFreeStringAnalysisState = ContextFreeStringAnalysisState(vd, ps(vd.m, TACAI.key)) + + if (state.tacaiDependee.isRefinable) { + InterimResult( + state.entity, + StringConstancyProperty.lb, + StringConstancyProperty.ub, + state.dependees, + continuation(state) + ) + } else if (state.tacaiDependee.ub.tac.isEmpty) { + // No TAC available, e.g., because the method has no body + Result( + state.entity, + if (highSoundness) StringConstancyProperty.lb + else StringConstancyProperty.ub + ) + } else { + computeResults + } + } + + private def continuation(state: ContextFreeStringAnalysisState)(eps: SomeEPS): ProperPropertyComputationResult = { + eps match { + case _ if eps.pk == TACAI.key => + state.tacaiDependee = eps.asInstanceOf[EOptionP[Method, TACAI]] + computeResults(state) + + case _ if eps.pk == MethodStringFlow.key => + state.stringFlowDependee = Some(eps.asInstanceOf[EOptionP[Method, MethodStringFlow]]) + computeResults(state) + + case _ => + throw new IllegalArgumentException(s"Unexpected eps in continuation: $eps") + } + } + + private def computeResults(implicit state: ContextFreeStringAnalysisState): ProperPropertyComputationResult = { + val newUB = StringConstancyProperty( + if (state.stringFlowDependee.isEmpty) computeUBTreeUsingTACAI + else computeUBTreeUsingStringFlow + ) + + if (state.hasDependees && !state.hitDepthThreshold) { + InterimResult( + state.entity, + StringConstancyProperty.lb, + newUB, + state.dependees, + continuation(state) + ) + } else { + Result(state.entity, newUB) + } + } + + private def computeUBTreeUsingTACAI(implicit state: ContextFreeStringAnalysisState): StringTreeNode = { + val tac = state.tacaiDependee.ub.tac.get + + def mapDefPCToStringTree(defPC: Int): Option[StringTreeNode] = { + if (defPC < 0) { + None + } else { + tac.stmts(valueOriginOfPC(defPC, tac.pcToIndex).get).asAssignment.expr match { + case StringConst(_, v) => Some(StringTreeConst(v)) + case _ => None + } + } + } + + val treeOpts = state.entity.pv match { + case PUVar(_, defPCs) => + defPCs.map(pc => mapDefPCToStringTree(pc)) + + case PDVar(_, _) => + Set(mapDefPCToStringTree(state.entity.pc)) + } + + if (treeOpts.exists(_.isEmpty)) { + // Could not resolve all values immediately (i.e. non-literal strings), so revert to computing string flow + state.stringFlowDependee = Some(ps(state.entity.m, MethodStringFlow.key)) + computeUBTreeUsingStringFlow + } else { + StringTreeOr(treeOpts.map(_.get)) + } + } + + private def computeUBTreeUsingStringFlow(implicit state: ContextFreeStringAnalysisState): StringTreeNode = { + if (state.stringFlowDependee.isEmpty) { + throw new IllegalStateException(s"Requested to compute an UB using method string flow but none is given!") + } + + state.stringFlowDependee.get match { + case UBP(methodStringFlow) => + val tree = methodStringFlow(state.entity.pc, state.entity.pv).simplify + if (tree.depth >= depthThreshold) { + // String constancy information got too complex, abort. This guard can probably be removed once + // recursing functions are properly handled using e.g. the widen-converge approach. + state.hitDepthThreshold = true + if (highSoundness) { + tree.replaceAtDepth(depthThreshold, StringTreeNode.lb) + } else { + // In low soundness, we cannot decrease the matched string values by limiting the string tree + // with the upper bound. We should also not limit it with the lower bound, since that would + // make the string tree at least partially dynamic and cause other low soundness analysis to + // abort. Thus, return the tree itself as the final value. + tree + } + } else { + tree + } + case _: EPK[_, MethodStringFlow] => + StringTreeNode.ub + } + } +} + +/** + * Analyzes a given variable in context of its method in a context-sensitive manner, i.e. resolving all method + * parameter references given in the string tree that was resolved for the variable from the + * [[ContextFreeStringAnalysis]]. + * + * @see [[ContextFreeStringAnalysis]], [[MethodParameterContextStringAnalysis]] + * + * @author Maximilian Rüsch + */ +class ContextStringAnalysis(override val project: SomeProject) extends StringAnalysis { + + def analyze(vc: VariableContext): ProperPropertyComputationResult = { + val vdScp = ps(VariableDefinition(vc.pc, vc.pv, vc.m), StringConstancyProperty.key) + + implicit val state: ContextStringAnalysisState = ContextStringAnalysisState(vc, vdScp) + state.parameterIndices.foreach { index => + val mpcDependee = ps(MethodParameterContext(index, state.entity.context), StringConstancyProperty.key) + state.registerParameterDependee(mpcDependee) + } + + computeResults + } + + private def continuation(state: ContextStringAnalysisState)(eps: SomeEPS): ProperPropertyComputationResult = { + implicit val _state: ContextStringAnalysisState = state + eps match { + // "Downwards" dependency + case EUBP(_: VariableDefinition, _: StringConstancyProperty) => + handleStringDefinitionUpdate(eps.asInstanceOf[EPS[VariableDefinition, StringConstancyProperty]]) + computeResults + + // "Upwards" dependency + case EUBP(_: MethodParameterContext, _: StringConstancyProperty) => + state.updateParamDependee(eps.asInstanceOf[EOptionP[MethodParameterContext, StringConstancyProperty]]) + computeResults + + case _ => + throw new IllegalArgumentException(s"Unexpected eps in continuation: $eps") + } + } + + private def handleStringDefinitionUpdate( + newDefinitionEPS: EPS[VariableDefinition, StringConstancyProperty] + )(implicit state: ContextStringAnalysisState): Unit = { + val previousIndices = state.parameterIndices + state.updateStringDependee(newDefinitionEPS) + val newIndices = state.parameterIndices + + newIndices.diff(previousIndices).foreach { index => + val mpcDependee = ps(MethodParameterContext(index, state.entity.context), StringConstancyProperty.key) + state.registerParameterDependee(mpcDependee) + } + } + + private def computeResults(implicit state: ContextStringAnalysisState): ProperPropertyComputationResult = { + if (state.hasDependees) { + InterimResult( + state.entity, + StringConstancyProperty.lb, + StringConstancyProperty(state.currentTreeUB), + state.dependees, + continuation(state) + ) + } else { + Result(state.entity, StringConstancyProperty(state.currentTreeUB)) + } + } +} + +/** + * Analyzes the possible values of a given method parameter by analyzing the actual method parameters at all call sites + * of the given method using the [[ContextStringAnalysis]]. Uses the call graph to find all call sites of the given + * method. + * + * @see [[ContextStringAnalysis]] + * + * @author Maximilian Rüsch + */ +private[string] class MethodParameterContextStringAnalysis(override val project: SomeProject) extends StringAnalysis { + + private implicit val contextProvider: ContextProvider = project.get(ContextProviderKey) + + def analyze(mpc: MethodParameterContext): ProperPropertyComputationResult = { + implicit val state: MethodParameterContextStringAnalysisState = MethodParameterContextStringAnalysisState(mpc) + + val callersEOptP = ps(state.dm, Callers.key) + state.updateCallers(callersEOptP) + if (callersEOptP.hasUBP) { + handleNewCallers(NoCallers, callersEOptP.ub) + } + + computeResults + } + + private def continuation(state: MethodParameterContextStringAnalysisState)( + eps: SomeEPS + ): ProperPropertyComputationResult = { + implicit val _state: MethodParameterContextStringAnalysisState = state + eps match { + case UBP(callers: Callers) => + val oldCallers = if (state.callersDependee.get.hasUBP) state.callersDependee.get.ub else NoCallers + state.updateCallers(eps.asInstanceOf[EOptionP[DeclaredMethod, Callers]]) + handleNewCallers(oldCallers, callers) + computeResults + + case EUBP(m: Method, tacai: TACAI) => + if (tacai.tac.isEmpty) { + state.discoveredUnknownTAC = true + } else { + state.getCallerContexts(m).foreach(handleTACForContext(tacai.tac.get, _)) + } + computeResults + + case EUBP(_: VariableContext, _: StringConstancyProperty) => + state.updateParamDependee(eps.asInstanceOf[EOptionP[VariableContext, StringConstancyProperty]]) + computeResults + + case _ => + throw new IllegalArgumentException(s"Unexpected eps in continuation: $eps") + } + } + + private def handleNewCallers( + oldCallers: Callers, + newCallers: Callers + )(implicit state: MethodParameterContextStringAnalysisState): Unit = { + val relevantCallerContexts = ListBuffer.empty[(Context, Int)] + newCallers.forNewCallerContexts(oldCallers, state.dm) { (_, callerContext, pc, _) => + if (callerContext.hasContext && callerContext.method.hasSingleDefinedMethod) { + relevantCallerContexts.append((callerContext, pc)) + } + } + + for { callerContext <- relevantCallerContexts } { + state.addCallerContext(callerContext) + val tacEOptP = state.getTacaiForContext(callerContext) + if (tacEOptP.hasUBP) { + val tacOpt = tacEOptP.ub.tac + if (tacOpt.isEmpty) { + state.discoveredUnknownTAC = true + } else { + handleTACForContext(tacOpt.get, callerContext) + } + } + } + } + + private def handleTACForContext( + tac: TAC, + context: (Context, Int) + )(implicit state: MethodParameterContextStringAnalysisState): Unit = { + val callExpr = tac.stmts(valueOriginOfPC(context._2, tac.pcToIndex).get) match { + case Assignment(_, _, expr) if expr.isInstanceOf[Call[_]] => expr.asInstanceOf[Call[V]] + case ExprStmt(_, expr) if expr.isInstanceOf[Call[_]] => expr.asInstanceOf[Call[V]] + case call: Call[_] => call.asInstanceOf[Call[V]] + case node => throw new IllegalArgumentException(s"Unexpected argument: $node") + } + + val previousCallExpr = state.addCallExprInformationForContext(context, callExpr) + if (previousCallExpr.isEmpty || previousCallExpr.get != callExpr) { + handleCallExpr(context, callExpr) + } + } + + private def handleCallExpr( + callerContext: (Context, Int), + callExpr: Call[V] + )(implicit state: MethodParameterContextStringAnalysisState): Unit = { + val dm = callerContext._1.method.asDefinedMethod + val tac = state.getTACForContext(callerContext) + val paramVC = VariableContext( + callerContext._2, + callExpr.params(state.index).asVar.toPersistentForm(tac.stmts), + callerContext._1 + ) + state.registerParameterDependee(dm, ps(paramVC, StringConstancyProperty.key)) + } + + private def computeResults(implicit + state: MethodParameterContextStringAnalysisState + ): ProperPropertyComputationResult = { + if (state.hasDependees) { + InterimResult( + state.entity, + StringConstancyProperty.lb, + StringConstancyProperty(state.currentTreeUB), + state.dependees, + continuation(state) + ) + } else { + Result(state.entity, StringConstancyProperty(state.currentTreeUB)) + } + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisConfig.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisConfig.scala new file mode 100644 index 0000000000..8809d06b75 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisConfig.scala @@ -0,0 +1,46 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string + +import org.opalj.br.analyses.SomeProject +import org.opalj.log.Error +import org.opalj.log.Info +import org.opalj.log.LogContext +import org.opalj.log.OPALLogger.logOnce + +/** + * Shared config between the multiple analyses of the string analysis package. + * + * @author Maximilian Rüsch + */ +trait StringAnalysisConfig { + + val project: SomeProject + implicit def logContext: LogContext + + private final val ConfigLogCategory = "analysis configuration - string analysis - universal" + + implicit val highSoundness: Boolean = { + val isHighSoundness = + try { + project.config.getBoolean(StringAnalysisConfig.HighSoundnessConfigKey) + } catch { + case t: Throwable => + logOnce { + Error(ConfigLogCategory, s"couldn't read: ${StringAnalysisConfig.HighSoundnessConfigKey}", t) + } + false + } + + logOnce(Info(ConfigLogCategory, s"using ${if (isHighSoundness) "high" else "low"} soundness mode")) + isHighSoundness + } +} + +object StringAnalysisConfig { + + final val HighSoundnessConfigKey = "org.opalj.fpcf.analyses.string.highSoundness" +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisScheduler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisScheduler.scala new file mode 100644 index 0000000000..05017ab7a0 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisScheduler.scala @@ -0,0 +1,75 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string + +import org.opalj.br.analyses.ProjectInformationKeys +import org.opalj.br.analyses.SomeProject +import org.opalj.br.fpcf.ContextProviderKey +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.FPCFAnalysisScheduler +import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler +import org.opalj.br.fpcf.properties.string.StringConstancyProperty +import org.opalj.fpcf.Entity +import org.opalj.fpcf.PropertyBounds +import org.opalj.fpcf.PropertyStore +import org.opalj.tac.fpcf.properties.string.MethodStringFlow + +/** + * A trait for FPCF analysis schedulers that combine the parts of the string analysis that produce + * [[StringConstancyProperty]]s. + * + * @author Maximilian Rüsch + */ +sealed trait StringAnalysisScheduler extends FPCFAnalysisScheduler { + + final def derivedProperty: PropertyBounds = PropertyBounds.lub(StringConstancyProperty) + + override def uses: Set[PropertyBounds] = Set(PropertyBounds.ub(MethodStringFlow)) + + override final type InitializationData = + (ContextFreeStringAnalysis, ContextStringAnalysis, MethodParameterContextStringAnalysis) + + override def init(p: SomeProject, ps: PropertyStore): InitializationData = ( + new ContextFreeStringAnalysis(p), + new ContextStringAnalysis(p), + new MethodParameterContextStringAnalysis(p) + ) + + override def beforeSchedule(p: SomeProject, ps: PropertyStore): Unit = {} + + override def afterPhaseScheduling(ps: PropertyStore, analysis: FPCFAnalysis): Unit = {} + + override def afterPhaseCompletion(p: SomeProject, ps: PropertyStore, analysis: FPCFAnalysis): Unit = {} +} + +/** + * A lazy adaptation of the [[StringAnalysisScheduler]]. All three string analyses are combined since the property store + * does not allow registering more than one lazy analysis scheduler for a given property. + * + * @author Maximilian Rüsch + */ +object LazyStringAnalysis + extends StringAnalysisScheduler with FPCFLazyAnalysisScheduler { + + override def register(p: SomeProject, ps: PropertyStore, data: InitializationData): FPCFAnalysis = { + ps.registerLazyPropertyComputation( + StringConstancyProperty.key, + (entity: Entity) => { + entity match { + case vd: VariableDefinition => data._1.analyze(vd) + case vc: VariableContext => data._2.analyze(vc) + case vc: MethodParameterContext => data._3.analyze(vc) + case e => throw new IllegalArgumentException(s"Cannot process entity $e") + } + } + ) + data._1 + } + + override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) + + override def requiredProjectInformation: ProjectInformationKeys = Seq(ContextProviderKey) +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisState.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisState.scala new file mode 100644 index 0000000000..e6eac2b89d --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringAnalysisState.scala @@ -0,0 +1,233 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string + +import scala.collection.mutable + +import org.opalj.br.DeclaredMethod +import org.opalj.br.DefinedMethod +import org.opalj.br.Method +import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.cg.Callers +import org.opalj.br.fpcf.properties.string.StringConstancyProperty +import org.opalj.br.fpcf.properties.string.StringTreeNode +import org.opalj.br.fpcf.properties.string.StringTreeOr +import org.opalj.fpcf.Entity +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.EPS +import org.opalj.fpcf.Property +import org.opalj.fpcf.PropertyStore +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.fpcf.properties.string.MethodStringFlow + +/** + * FPCF analysis state for the [[ContextFreeStringAnalysis]]. + * + * @see [[ContextFreeStringAnalysis]] + * + * @author Maximilian Rüsch + */ +private[string] case class ContextFreeStringAnalysisState( + entity: VariableDefinition, + var tacaiDependee: EOptionP[Method, TACAI], + var stringFlowDependee: Option[EOptionP[Method, MethodStringFlow]] = None, + var hitDepthThreshold: Boolean = false +) { + + def hasDependees: Boolean = tacaiDependee.isRefinable || stringFlowDependee.exists(_.isRefinable) + + def dependees: Set[EOptionP[Entity, Property]] = + Set(tacaiDependee).filter(_.isRefinable) ++ stringFlowDependee.filter(_.isRefinable) +} + +/** + * FPCF analysis state for the [[ContextStringAnalysis]]. Provides support for adding required parameter dependees due + * to changes in the string dependee at runtime. + * + * @see [[ContextStringAnalysis]] + * + * @author Maximilian Rüsch + */ +private[string] class ContextStringAnalysisState private ( + val entity: VariableContext, + private var _stringDependee: EOptionP[VariableDefinition, StringConstancyProperty], + private var _parameterIndices: Set[Int] +) { + + def updateStringDependee(stringDependee: EPS[VariableDefinition, StringConstancyProperty]): Unit = { + _stringDependee = stringDependee + _parameterIndices ++= stringDependee.ub.tree.parameterIndices + } + def parameterIndices: Set[Int] = _parameterIndices + + // Parameter StringConstancy + private val _paramDependees: mutable.Map[MethodParameterContext, EOptionP[ + MethodParameterContext, + StringConstancyProperty + ]] = + mutable.Map.empty + + def registerParameterDependee(dependee: EOptionP[MethodParameterContext, StringConstancyProperty]): Unit = { + if (_paramDependees.contains(dependee.e)) { + throw new IllegalArgumentException(s"Tried to register the same parameter dependee twice: $dependee") + } + + updateParamDependee(dependee) + } + def updateParamDependee(dependee: EOptionP[MethodParameterContext, StringConstancyProperty]): Unit = + _paramDependees(dependee.e) = dependee + + def currentTreeUB: StringTreeNode = { + if (_stringDependee.hasUBP) { + val paramTrees = _paramDependees.map { kv => + ( + kv._1.index, + if (kv._2.hasUBP) kv._2.ub.tree + else StringTreeNode.ub + ) + }.toMap + + _stringDependee.ub.tree.replaceParameters(paramTrees).simplify + } else { + StringTreeNode.ub + } + } + + def hasDependees: Boolean = _stringDependee.isRefinable || _paramDependees.valuesIterator.exists(_.isRefinable) + + def dependees: Set[EOptionP[Entity, Property]] = { + val preliminaryDependees = + _paramDependees.valuesIterator.filter(_.isRefinable) ++ + Some(_stringDependee).filter(_.isRefinable) + + preliminaryDependees.toSet + } +} + +object ContextStringAnalysisState { + + def apply( + entity: VariableContext, + stringDependee: EOptionP[VariableDefinition, StringConstancyProperty] + ): ContextStringAnalysisState = { + val parameterIndices = if (stringDependee.hasUBP) stringDependee.ub.tree.parameterIndices + else Set.empty[Int] + new ContextStringAnalysisState(entity, stringDependee, parameterIndices) + } +} + +/** + * FPCF analysis state for the [[MethodParameterContextStringAnalysis]]. Provides support for finding call sites for a + * given method as well as the relevant TACAI properties and their call parameter string dependees. + * + * @see [[MethodParameterContextStringAnalysis]] + * + * @author Maximilian Rüsch + */ +private[string] case class MethodParameterContextStringAnalysisState( + entity: MethodParameterContext +) { + + def dm: DeclaredMethod = entity.context.method + def index: Int = entity.index + + // Callers + private[string] type CallerContext = (Context, Int) + private var _callersDependee: Option[EOptionP[DeclaredMethod, Callers]] = None + private val _callerContexts: mutable.Map[CallerContext, Option[Call[V]]] = mutable.Map.empty + private val _callerContextsByMethod: mutable.Map[Method, Seq[CallerContext]] = mutable.Map.empty + + def callersDependee: Option[EOptionP[DeclaredMethod, Callers]] = _callersDependee + + def updateCallers(newCallers: EOptionP[DeclaredMethod, Callers]): Unit = _callersDependee = Some(newCallers) + + def addCallerContext(callerContext: CallerContext): Unit = { + _callerContexts.update(callerContext, None) + _callerContextsByMethod.updateWith(callerContext._1.method.definedMethod) { + case None => Some(Seq(callerContext)) + case Some(prev) => Some(callerContext +: prev) + } + } + def getCallerContexts(m: Method): Seq[CallerContext] = _callerContextsByMethod(m) + def addCallExprInformationForContext(callerContext: CallerContext, expr: Call[V]): Option[Call[V]] = + _callerContexts.put(callerContext, Some(expr)).flatten + + // TACAI + private val _tacaiDependees: mutable.Map[Method, EOptionP[Method, TACAI]] = mutable.Map.empty + var discoveredUnknownTAC: Boolean = false + + def updateTacaiDependee(tacEOptP: EOptionP[Method, TACAI]): Unit = _tacaiDependees(tacEOptP.e) = tacEOptP + def getTacaiForContext(callerContext: CallerContext)(implicit ps: PropertyStore): EOptionP[Method, TACAI] = { + val m = callerContext._1.method.definedMethod + + if (_tacaiDependees.contains(m)) { + _tacaiDependees(m) + } else { + val tacEOptP = ps(m, TACAI.key) + _tacaiDependees(tacEOptP.e) = tacEOptP + tacEOptP + } + } + def getTACForContext(callerContext: CallerContext)(implicit ps: PropertyStore): TAC = + getTacaiForContext(callerContext).ub.tac.get + + // Parameter StringConstancy + private val _methodToEntityMapping: mutable.Map[DefinedMethod, Seq[VariableContext]] = mutable.Map.empty + private val _paramDependees: mutable.Map[VariableContext, EOptionP[VariableContext, StringConstancyProperty]] = + mutable.Map.empty + + def registerParameterDependee( + dm: DefinedMethod, + dependee: EOptionP[VariableContext, StringConstancyProperty] + ): Unit = { + if (_paramDependees.contains(dependee.e)) { + throw new IllegalArgumentException(s"Tried to register the same parameter dependee twice: $dependee") + } + + _methodToEntityMapping.updateWith(dm) { + case None => Some(Seq(dependee.e)) + case Some(previous) => Some((previous :+ dependee.e).sortBy(_.pc)) + } + + updateParamDependee(dependee) + } + def updateParamDependee(dependee: EOptionP[VariableContext, StringConstancyProperty]): Unit = + _paramDependees(dependee.e) = dependee + + def currentTreeUB(implicit highSoundness: Boolean): StringTreeNode = { + var paramOptions = _methodToEntityMapping.keys.toSeq + .sortBy(_.id) + .flatMap { dm => + _methodToEntityMapping(dm) + .map(_paramDependees) + .filter(_.hasUBP) + .map(_.ub.tree) + } + + if (highSoundness && ( + discoveredUnknownTAC || + _callersDependee.exists(cd => cd.hasUBP && cd.ub.hasCallersWithUnknownContext) + ) + ) { + paramOptions :+= StringTreeNode.lb + } + + StringTreeOr(paramOptions).simplify + } + + def hasDependees: Boolean = _callersDependee.exists(_.isRefinable) || + _tacaiDependees.valuesIterator.exists(_.isRefinable) || + _paramDependees.valuesIterator.exists(_.isRefinable) + + def dependees: Set[EOptionP[Entity, Property]] = { + val preliminaryDependees = + _tacaiDependees.valuesIterator.filter(_.isRefinable) ++ + _paramDependees.valuesIterator.filter(_.isRefinable) ++ + _callersDependee.filter(_.isRefinable) + + preliminaryDependees.toSet + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringInterpreter.scala new file mode 100644 index 0000000000..5ab76d3eec --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/StringInterpreter.scala @@ -0,0 +1,129 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string + +import org.opalj.br.fpcf.properties.string.StringTreeNode +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.ProperPropertyComputationResult +import org.opalj.fpcf.Result +import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationHandler +import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState +import org.opalj.tac.fpcf.properties.string.StringFlowFunction +import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty + +/** + * The base trait for all string interpreters, producing a FPCF [[StringFlowFunctionProperty]] for a given statement + * in the context of its method. + * + * @author Maximilian Rüsch + */ +trait StringInterpreter { + + type T <: Stmt[V] + + /** + * @param instr The instruction that is to be interpreted. + * @return A [[ProperPropertyComputationResult]] for the given pc containing the interpretation of the given + * instruction in the form of a [[StringFlowFunctionProperty]]. + */ + def interpret(instr: T)(implicit state: InterpretationState): ProperPropertyComputationResult + + protected[this] def failureTree(implicit highSoundness: Boolean): StringTreeNode = + StringInterpreter.failureTree + + protected[this] def failure(v: PV)(implicit state: InterpretationState, highSoundness: Boolean): Result = + StringInterpreter.failure(v) + + protected[this] def computeFinalResult(web: PDUWeb, sff: StringFlowFunction)(implicit + state: InterpretationState + ): Result = + StringInterpreter.computeFinalResult(web, sff) + + protected[this] def computeFinalResult(webs: Set[PDUWeb], sff: StringFlowFunction)(implicit + state: InterpretationState + ): Result = + StringInterpreter.computeFinalResult(webs, sff) + + protected[this] def computeFinalResult(p: StringFlowFunctionProperty)(implicit state: InterpretationState): Result = + StringInterpreter.computeFinalResult(p) +} + +object StringInterpreter { + + def failureTree(implicit highSoundness: Boolean): StringTreeNode = { + if (highSoundness) StringTreeNode.lb + else StringTreeNode.ub + } + + def failure(v: V)(implicit state: InterpretationState, highSoundness: Boolean): Result = + failure(v.toPersistentForm(state.tac.stmts)) + + def failure(pv: PV)(implicit state: InterpretationState, highSoundness: Boolean): Result = + computeFinalResult(StringFlowFunctionProperty.constForVariableAt(state.pc, pv, failureTree)) + + def computeFinalResult(web: PDUWeb, sff: StringFlowFunction)(implicit state: InterpretationState): Result = + computeFinalResult(StringFlowFunctionProperty(web, sff)) + + def computeFinalResult(webs: Set[PDUWeb], sff: StringFlowFunction)(implicit state: InterpretationState): Result = + computeFinalResult(StringFlowFunctionProperty(webs, sff)) + + def computeFinalResult(p: StringFlowFunctionProperty)(implicit state: InterpretationState): Result = + Result(FinalEP(InterpretationHandler.getEntity(state), p)) +} + +/** + * Base trait for all [[StringInterpreter]]s that have to evaluate parameters at a given call site, thus providing + * appropriate utility. + * + * @author Maximilian Rüsch + */ +trait ParameterEvaluatingStringInterpreter extends StringInterpreter { + + protected def getParametersForPC(pc: Int)(implicit state: InterpretationState): Seq[Expr[V]] = { + state.tac.stmts(state.tac.pcToIndex(pc)) match { + case ExprStmt(_, vfc: FunctionCall[V]) => vfc.params + case Assignment(_, _, fc: FunctionCall[V]) => fc.params + case _ => Seq.empty + } + } +} + +/** + * Base trait for all string interpreters that only process [[AssignmentLikeStmt]]s, allowing the trait to pre-unpack + * the expression of the [[AssignmentLikeStmt]]. + * + * @author Maximilian Rüsch + */ +trait AssignmentLikeBasedStringInterpreter extends StringInterpreter { + + type E <: Expr[V] + + override type T <: AssignmentLikeStmt[V] + + override final def interpret(instr: T)(implicit state: InterpretationState): ProperPropertyComputationResult = + interpretExpr(instr, instr.expr.asInstanceOf[E]) + + def interpretExpr(instr: T, expr: E)(implicit state: InterpretationState): ProperPropertyComputationResult +} + +/** + * Base trait for all string interpreters that only process [[Assignment]]s, allowing the trait to pre-unpack the + * assignment target variable as well as the operation performed by [[AssignmentLikeBasedStringInterpreter]]. + * + * @author Maximilian Rüsch + */ +trait AssignmentBasedStringInterpreter extends AssignmentLikeBasedStringInterpreter { + + override type T = Assignment[V] + + override final def interpretExpr(instr: T, expr: E)(implicit + state: InterpretationState + ): ProperPropertyComputationResult = { + interpretExpr(instr.targetVar.toPersistentForm(state.tac.stmts), expr) + } + + def interpretExpr(target: PV, expr: E)(implicit state: InterpretationState): ProperPropertyComputationResult +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/DataFlowAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/DataFlowAnalysis.scala new file mode 100644 index 0000000000..1505634cab --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/DataFlowAnalysis.scala @@ -0,0 +1,223 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package flowanalysis + +import scala.collection.mutable + +import org.opalj.br.fpcf.properties.string.StringTreeDynamicString +import org.opalj.br.fpcf.properties.string.StringTreeInvalidElement +import org.opalj.tac.fpcf.properties.string.StringFlowFunction +import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment + +import scalax.collection.GraphTraversal.BreadthFirst +import scalax.collection.GraphTraversal.Parameters + +/** + * Performs structural string data flow analysis based on the results of a [[StructuralAnalysis]]. In more detail, this + * means that the control tree produced by the [[StructuralAnalysis]] is traversed recursively in a depth-first manner. + * Individual regions are processed by piping a [[StringTreeEnvironment]] through their nodes and joining the + * environments where paths meet up. Thus, the individual flow functions defined at the statement PCs of a method are + * combined using region-type-specific patterns to effectively act as a string flow function of the entire region, which + * is then processed itself due to the recursive nature of the algorithm. + * + * @param controlTree The control tree from the structural analysis. + * @param superFlowGraph The super flow graph from the structural analysis + * @param highSoundness Whether to use high soundness mode or not. Currently, this influences the handling of loops, + * i.e. whether they are approximated by one execution of the loop body (low soundness) or via + * "any string" on all variables in the method (high soundness). + * + * @see [[StructuralAnalysis]], [[StringTreeEnvironment]] + * + * @author Maximilian Rüsch + */ +class DataFlowAnalysis( + private val controlTree: ControlTree, + private val superFlowGraph: SuperFlowGraph, + private val highSoundness: Boolean +) { + + private val _nodeOrderings = mutable.Map.empty[FlowGraphNode, Seq[SuperFlowGraph#NodeT]] + private val _removedBackEdgesGraphs = mutable.Map.empty[FlowGraphNode, (Boolean, SuperFlowGraph)] + + /** + * Computes the resulting string tree environment after the data flow analysis. + * + * @param flowFunctionByPc A mapping from PC to the flow functions to be used + * @param startEnv The base environment which is piped into the first region to be processed. + * @return The resulting [[StringTreeEnvironment]] after execution of all string flow functions using the region + * hierarchy given in the control tree. + */ + def compute( + flowFunctionByPc: Map[Int, StringFlowFunction] + )(startEnv: StringTreeEnvironment): StringTreeEnvironment = { + val startNodeCandidates = controlTree.nodes.filter(!_.hasPredecessors) + if (startNodeCandidates.size != 1) { + throw new IllegalStateException("Found more than one start node in the control tree!") + } + + val startNode = startNodeCandidates.head.outer + pipeThroughNode(flowFunctionByPc)(startNode, startEnv) + } + + private def pipeThroughNode(flowFunctionByPc: Map[Int, StringFlowFunction])( + node: FlowGraphNode, + env: StringTreeEnvironment + ): StringTreeEnvironment = { + val pipe = pipeThroughNode(flowFunctionByPc) _ + val innerChildNodes = controlTree.get(node).diSuccessors.map(n => superFlowGraph.get(n.outer)) + + def processBlock(entry: FlowGraphNode): StringTreeEnvironment = { + var currentEnv = env + for { + currentNode <- superFlowGraph.innerNodeTraverser( + superFlowGraph.get(entry), + subgraphNodes = innerChildNodes.contains + ) + } { + currentEnv = pipe(currentNode.outer, currentEnv) + } + currentEnv + } + + def processIfThenElse(entry: FlowGraphNode): StringTreeEnvironment = { + val entryNode = superFlowGraph.get(entry) + val successors = entryNode.diSuccessors.intersect(innerChildNodes).map(_.outer).toList.sorted + val branches = (successors.head, successors.tail.head) + + val envAfterEntry = pipe(entry, env) + val envAfterBranches = (pipe(branches._1, envAfterEntry), pipe(branches._2, envAfterEntry)) + + envAfterBranches._1.join(envAfterBranches._2) + } + + def processIfThen(entry: FlowGraphNode): StringTreeEnvironment = { + val limitedFlowGraph = superFlowGraph.filter(innerChildNodes.contains) + val entryNode = limitedFlowGraph.get(entry) + val (yesBranch, noBranch) = if (entryNode.diSuccessors.head.diSuccessors.nonEmpty) { + (entryNode.diSuccessors.head, entryNode.diSuccessors.tail.head) + } else { + (entryNode.diSuccessors.tail.head, entryNode.diSuccessors.head) + } + + val envAfterEntry = pipe(entry, env) + val envAfterBranches = ( + pipe(yesBranch.diSuccessors.head, pipe(yesBranch, envAfterEntry)), + pipe(noBranch, envAfterEntry) + ) + + envAfterBranches._1.join(envAfterBranches._2) + } + + def handleProperSubregion[A <: FlowGraphNode, G <: SuperFlowGraph]( + g: G, + innerNodes: Set[G#NodeT], + entry: A + ): StringTreeEnvironment = { + val entryNode = g.get(entry) + val sortedNodes = _nodeOrderings.getOrElseUpdate( + node, { + val ordering = g.NodeOrdering((in1, in2) => in1.compare(in2)) + val traverser = entryNode.innerNodeTraverser(Parameters(BreadthFirst)) + .withOrdering(ordering) + .withSubgraph(nodes = innerNodes.contains) + // We know that the graph is acyclic here, so we can be sure that the topological sort never fails + traverser.topologicalSort().toOption.get.toSeq + } + ) + + val currentNodeEnvs = mutable.Map((entryNode, pipe(entry, env))) + for { + _currentNode <- sortedNodes.filter(_ != entryNode) + currentNode = _currentNode.asInstanceOf[g.NodeT] + } { + val previousEnvs = currentNode.diPredecessors.toList.map { dp => + pipe(currentNode.outer, currentNodeEnvs(dp)) + } + currentNodeEnvs.update(currentNode, StringTreeEnvironment.joinMany(previousEnvs)) + } + + currentNodeEnvs(sortedNodes.last.asInstanceOf[g.NodeT]) + } + + def processProper(entry: FlowGraphNode): StringTreeEnvironment = { + handleProperSubregion[FlowGraphNode, superFlowGraph.type](superFlowGraph, innerChildNodes, entry) + } + + def processSelfLoop(entry: FlowGraphNode): StringTreeEnvironment = { + val resultEnv = pipe(entry, env) + // IMPROVE only update affected variables instead of all + if (resultEnv != env && highSoundness) env.updateAll(StringTreeDynamicString) + else resultEnv + } + + def processWhileLoop(entry: FlowGraphNode): StringTreeEnvironment = { + val entryNode = superFlowGraph.get(entry) + val envAfterEntry = pipe(entry, env) + + superFlowGraph.innerNodeTraverser(entryNode, subgraphNodes = innerChildNodes) + + var resultEnv = env + for { + currentNode <- superFlowGraph + .innerNodeTraverser(entryNode) + .withSubgraph(n => n != entryNode && innerChildNodes.contains(n)) + } { + resultEnv = pipe(currentNode.outer, resultEnv) + } + + // IMPROVE only update affected variables instead of all + if (resultEnv != envAfterEntry && highSoundness) envAfterEntry.updateAll(StringTreeDynamicString) + else resultEnv + } + + def processNaturalLoop(entry: FlowGraphNode): StringTreeEnvironment = { + val (isCyclic, removedBackEdgesGraph) = _removedBackEdgesGraphs.getOrElseUpdate( + node, { + val limitedFlowGraph = superFlowGraph.filter(innerChildNodes.contains) + val entryPredecessors = limitedFlowGraph.get(entry).diPredecessors + val computedRemovedBackEdgesGraph = limitedFlowGraph.filterNot( + edgeP = edge => + edge.sources.toList.toSet.intersect(entryPredecessors).nonEmpty + && edge.targets.contains(limitedFlowGraph.get(entry)) + ) + (computedRemovedBackEdgesGraph.isCyclic, computedRemovedBackEdgesGraph) + } + ) + + if (isCyclic) { + // IMPROVE only update affected variables instead of all + if (highSoundness) env.updateAll(StringTreeDynamicString) + else env.updateAll(StringTreeInvalidElement) + } else { + // Handle resulting acyclic region + val resultEnv = handleProperSubregion[FlowGraphNode, removedBackEdgesGraph.type]( + removedBackEdgesGraph, + removedBackEdgesGraph.nodes.toSet, + entry + ) + // IMPROVE only update affected variables instead of all + if (resultEnv != env && highSoundness) env.updateAll(StringTreeDynamicString) + else resultEnv + } + } + + node match { + case Statement(pc) if pc >= 0 => flowFunctionByPc(pc)(env) + case Statement(_) => env + + case Region(Block, _, entry) => processBlock(entry) + case Region(IfThenElse, _, entry) => processIfThenElse(entry) + case Region(IfThen, _, entry) => processIfThen(entry) + case Region(Proper, _, entry) => processProper(entry) + case Region(SelfLoop, _, entry) => processSelfLoop(entry) + case Region(WhileLoop, _, entry) => processWhileLoop(entry) + case Region(NaturalLoop, _, entry) => processNaturalLoop(entry) + + case _ => env + } + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/FlowGraphNode.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/FlowGraphNode.scala new file mode 100644 index 0000000000..88f4cea03c --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/FlowGraphNode.scala @@ -0,0 +1,85 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package flowanalysis + +sealed trait RegionType extends Product + +sealed trait AcyclicRegionType extends RegionType +sealed trait CyclicRegionType extends RegionType + +case object Block extends AcyclicRegionType +case object IfThen extends AcyclicRegionType +case object IfThenElse extends AcyclicRegionType +case object Case extends AcyclicRegionType +case object Proper extends AcyclicRegionType +case object SelfLoop extends CyclicRegionType +case object WhileLoop extends CyclicRegionType +case object NaturalLoop extends CyclicRegionType +case object Improper extends CyclicRegionType + +/** + * A node in a flow graph and control tree produced by the [[StructuralAnalysis]]. + * + * @author Maximilian Rüsch + */ +sealed trait FlowGraphNode extends Ordered[FlowGraphNode] { + + def nodeIds: Set[Int] + + override def compare(that: FlowGraphNode): Int = nodeIds.toList.min.compare(that.nodeIds.toList.min) +} + +/** + * Represents a region of nodes in a [[FlowGraph]], consisting of multiple sub-nodes. Can identify general acyclic and + * cyclic structures or more specialised instances of such structures such as [[IfThenElse]] or [[WhileLoop]]. + * + * @param regionType The type of the region. + * @param nodeIds The union of all ids the leafs that are contained in this region. + * @param entry The direct child of this region that contains the first leaf to be executed when entering the region. + */ +case class Region(regionType: RegionType, override val nodeIds: Set[Int], entry: FlowGraphNode) extends FlowGraphNode { + + override def toString: String = + s"Region(${regionType.productPrefix}; ${nodeIds.toList.sorted.mkString(",")}; ${entry.toString})" + + // Performance optimizations + private lazy val _hashCode = scala.util.hashing.MurmurHash3.productHash(this) + override def hashCode(): Int = _hashCode + override def canEqual(obj: Any): Boolean = obj.hashCode() == _hashCode +} + +/** + * Represents a single statement in a methods [[FlowGraph]] and is one of the leaf nodes to be grouped by a [[Region]]. + * + * @param pc The PC that the statement is given at. + */ +case class Statement(pc: Int) extends FlowGraphNode { + + override val nodeIds: Set[Int] = Set(pc) + + override def toString: String = s"Statement($pc)" +} + +/** + * An additional global entry node to a methods [[FlowGraph]] to ensure only one entry node exists. + */ +object GlobalEntry extends FlowGraphNode { + + override val nodeIds: Set[Int] = Set(Int.MinValue + 1) + + override def toString: String = "GlobalEntry" +} + +/** + * An additional global exit node to a methods [[FlowGraph]] to ensure only one exit node exists. + */ +object GlobalExit extends FlowGraphNode { + + override val nodeIds: Set[Int] = Set(Int.MinValue) + + override def toString: String = "GlobalExit" +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysis.scala new file mode 100644 index 0000000000..d35e367220 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysis.scala @@ -0,0 +1,157 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package flowanalysis + +import scala.jdk.CollectionConverters._ + +import org.opalj.br.Method +import org.opalj.br.analyses.DeclaredMethods +import org.opalj.br.analyses.DeclaredMethodsKey +import org.opalj.br.analyses.SomeProject +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.EUBP +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.FinalP +import org.opalj.fpcf.InterimResult +import org.opalj.fpcf.ProperPropertyComputationResult +import org.opalj.fpcf.Result +import org.opalj.fpcf.SomeEPS +import org.opalj.log.Error +import org.opalj.log.Info +import org.opalj.log.OPALLogger.logOnce +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.fpcf.properties.string.MethodStringFlow +import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty + +/** + * Analyzes a methods string flow results by applying a [[StructuralAnalysis]] to identify all control flow regions of + * the methods CFG and subsequently applying a [[DataFlowAnalysis]] to compute a resulting string tree environment + * using string flow functions derived from the FPCF [[StringFlowFunctionProperty]]. + * + * @note Packages can be configured to be excluded from analysis entirely due to e.g. size problems. In these cases, the + * lower or upper bound string tree environment will be returned, depending on the soundness mode of the analysis. + * + * @see [[StructuralAnalysis]], [[DataFlowAnalysis]], [[StringFlowFunctionProperty]], [[StringAnalysisConfig]] + * + * @author Maximilian Rüsch + */ +class MethodStringFlowAnalysis(override val project: SomeProject) extends FPCFAnalysis with StringAnalysisConfig { + + private final val ConfigLogCategory = "analysis configuration - method string flow analysis" + + private val excludedPackages: Seq[String] = { + val packages = + try { + project.config.getStringList(MethodStringFlowAnalysis.ExcludedPackagesConfigKey).asScala + } catch { + case t: Throwable => + logOnce { + Error( + ConfigLogCategory, + s"couldn't read: ${MethodStringFlowAnalysis.ExcludedPackagesConfigKey}", + t + ) + } + Seq.empty[String] + } + + logOnce(Info(ConfigLogCategory, s"${packages.size} packages are excluded from string flow analysis")) + packages.toSeq + } + + val declaredMethods: DeclaredMethods = project.get(DeclaredMethodsKey) + + def analyze(method: Method): ProperPropertyComputationResult = { + val state = MethodStringFlowAnalysisState(method, declaredMethods(method), ps(method, TACAI.key)) + + if (excludedPackages.exists(method.classFile.thisType.packageName.startsWith(_))) { + Result( + state.entity, + if (highSoundness) MethodStringFlow.lb + else MethodStringFlow.ub + ) + } else if (state.tacDependee.isRefinable) { + InterimResult( + state.entity, + MethodStringFlow.lb, + MethodStringFlow.ub, + Set(state.tacDependee), + continuation(state) + ) + } else if (state.tacDependee.ub.tac.isEmpty) { + // No TAC available, e.g., because the method has no body + Result( + state.entity, + if (highSoundness) MethodStringFlow.lb + else MethodStringFlow.ub + ) + } else { + determinePossibleStrings(state) + } + } + + private def determinePossibleStrings(implicit + state: MethodStringFlowAnalysisState + ): ProperPropertyComputationResult = { + implicit val tac: TAC = state.tac + + state.flowGraph = FlowGraph(tac.cfg) + val (_, superFlowGraph, controlTree) = + StructuralAnalysis.analyze(state.flowGraph, FlowGraph.entry) + state.superFlowGraph = superFlowGraph + state.controlTree = controlTree + state.flowAnalysis = new DataFlowAnalysis(state.controlTree, state.superFlowGraph, highSoundness) + + state.flowGraph.nodes.toOuter.foreach { + case Statement(pc) if pc >= 0 => + state.updateDependee(pc, propertyStore(MethodPC(pc, state.dm), StringFlowFunctionProperty.key)) + + case _ => + } + + computeResults + } + + private def continuation(state: MethodStringFlowAnalysisState)(eps: SomeEPS): ProperPropertyComputationResult = { + eps match { + case FinalP(_: TACAI) if eps.pk.equals(TACAI.key) => + state.tacDependee = eps.asInstanceOf[FinalEP[Method, TACAI]] + determinePossibleStrings(state) + + case EUBP(e: MethodPC, _: StringFlowFunctionProperty) if eps.pk.equals(StringFlowFunctionProperty.key) => + state.updateDependee(e.pc, eps.asInstanceOf[EOptionP[MethodPC, StringFlowFunctionProperty]]) + computeResults(state) + + case _ => + throw new IllegalArgumentException(s"Unknown EPS given in continuation: $eps") + } + } + + private def computeResults(implicit state: MethodStringFlowAnalysisState): ProperPropertyComputationResult = { + if (state.hasDependees) { + InterimResult.forUB( + state.entity, + computeNewUpperBound(state), + state.dependees.toSet, + continuation(state) + ) + } else { + Result(state.entity, computeNewUpperBound(state)) + } + } + + private def computeNewUpperBound(state: MethodStringFlowAnalysisState): MethodStringFlow = { + val startEnv = state.getStartEnvAndReset + MethodStringFlow(state.flowAnalysis.compute(state.getFlowFunctionsByPC)(startEnv)) + } +} + +object MethodStringFlowAnalysis { + + final val ExcludedPackagesConfigKey = "org.opalj.fpcf.analyses.string.MethodStringFlowAnalysis.excludedPackages" +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisScheduler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisScheduler.scala new file mode 100644 index 0000000000..3cddd596e4 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisScheduler.scala @@ -0,0 +1,58 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package flowanalysis + +import org.opalj.br.analyses.DeclaredMethodsKey +import org.opalj.br.analyses.ProjectInformationKeys +import org.opalj.br.analyses.SomeProject +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.FPCFAnalysisScheduler +import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler +import org.opalj.fpcf.PropertyBounds +import org.opalj.fpcf.PropertyStore +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.fpcf.properties.string.MethodStringFlow +import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty + +/** + * A shared scheduler trait for analyses that analyse the string flow of given methods. + * + * @see [[MethodStringFlowAnalysis]] + * + * @author Maximilian Rüsch + */ +sealed trait MethodStringFlowAnalysisScheduler extends FPCFAnalysisScheduler { + + final def derivedProperty: PropertyBounds = PropertyBounds.ub(MethodStringFlow) + + override def uses: Set[PropertyBounds] = Set( + PropertyBounds.ub(TACAI), + PropertyBounds.ub(StringFlowFunctionProperty) + ) + + override final type InitializationData = MethodStringFlowAnalysis + override def init(p: SomeProject, ps: PropertyStore): InitializationData = new MethodStringFlowAnalysis(p) + + override def beforeSchedule(p: SomeProject, ps: PropertyStore): Unit = {} + + override def afterPhaseScheduling(ps: PropertyStore, analysis: FPCFAnalysis): Unit = {} + + override def afterPhaseCompletion(p: SomeProject, ps: PropertyStore, analysis: FPCFAnalysis): Unit = {} +} + +object LazyMethodStringFlowAnalysis + extends MethodStringFlowAnalysisScheduler with FPCFLazyAnalysisScheduler { + + override def register(p: SomeProject, ps: PropertyStore, initData: InitializationData): FPCFAnalysis = { + ps.registerLazyPropertyComputation(MethodStringFlow.key, initData.analyze) + initData + } + + override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) + + override def requiredProjectInformation: ProjectInformationKeys = Seq(DeclaredMethodsKey) +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisState.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisState.scala new file mode 100644 index 0000000000..cde53ad181 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/MethodStringFlowAnalysisState.scala @@ -0,0 +1,123 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package flowanalysis + +import scala.collection.mutable + +import org.opalj.ai.ImmediateVMExceptionsOriginOffset +import org.opalj.br.DefinedMethod +import org.opalj.br.Method +import org.opalj.br.fpcf.properties.string.StringTreeInvalidElement +import org.opalj.br.fpcf.properties.string.StringTreeParameter +import org.opalj.collection.immutable.IntTrieSet +import org.opalj.fpcf.EOptionP +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.fpcf.properties.string.StringFlowFunction +import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty +import org.opalj.tac.fpcf.properties.string.StringTreeEnvironment + +/** + * The state of the [[MethodStringFlowAnalysis]] containing data flow analysis relevant information such as flow graphs + * and all string flow dependees for the method under analysis. + * + * @see [[MethodStringFlowAnalysis]] + * + * @author Maximilian Rüsch + */ +case class MethodStringFlowAnalysisState(entity: Method, dm: DefinedMethod, var tacDependee: EOptionP[Method, TACAI]) { + + def tac: TAC = { + if (tacDependee.hasUBP && tacDependee.ub.tac.isDefined) + tacDependee.ub.tac.get + else + throw new IllegalStateException("Cannot get a TAC from a TACAI with no or empty upper bound!") + } + + var flowGraph: FlowGraph = _ + var superFlowGraph: SuperFlowGraph = _ + var controlTree: ControlTree = _ + var flowAnalysis: DataFlowAnalysis = _ + + private val pcToDependeeMapping: mutable.Map[Int, EOptionP[MethodPC, StringFlowFunctionProperty]] = + mutable.Map.empty + private val pcToWebChangeMapping: mutable.Map[Int, Boolean] = mutable.Map.empty + + def updateDependee(pc: Int, dependee: EOptionP[MethodPC, StringFlowFunctionProperty]): Unit = { + val prevOpt = pcToDependeeMapping.get(pc) + if (prevOpt.isEmpty || prevOpt.get.hasNoUBP || (dependee.hasUBP && prevOpt.get.ub.webs != dependee.ub.webs)) { + pcToWebChangeMapping.update(pc, true) + } + pcToDependeeMapping.update(pc, dependee) + } + + def dependees: Set[EOptionP[MethodPC, StringFlowFunctionProperty]] = + pcToDependeeMapping.values.filter(_.isRefinable).toSet + + def hasDependees: Boolean = pcToDependeeMapping.valuesIterator.exists(_.isRefinable) + + def getFlowFunctionsByPC: Map[Int, StringFlowFunction] = pcToDependeeMapping.toMap.transform { (_, eOptP) => + if (eOptP.hasUBP) eOptP.ub.flow + else StringFlowFunctionProperty.ub.flow + } + + private def webs: IndexedSeq[PDUWeb] = { + pcToDependeeMapping.values.flatMap { v => + if (v.hasUBP) v.ub.webs + else StringFlowFunctionProperty.ub.webs + }.toIndexedSeq.appendedAll(tac.params.parameters.zipWithIndex.map { + case (param, index) => + PDUWeb(IntTrieSet(-index - 1), if (param != null) param.useSites else IntTrieSet.empty) + }) + } + + private var _startEnv: StringTreeEnvironment = StringTreeEnvironment(Map.empty) + def getStartEnvAndReset(implicit highSoundness: Boolean): StringTreeEnvironment = { + if (pcToWebChangeMapping.exists(_._2)) { + val indexedWebs = mutable.ArrayBuffer.empty[PDUWeb] + val defPCToWebIndex = mutable.Map.empty[Int, Int] + webs.foreach { web => + val existingDefPCs = web.defPCs.filter(defPCToWebIndex.contains) + if (existingDefPCs.nonEmpty) { + val indices = existingDefPCs.toList.map(defPCToWebIndex).distinct + if (indices.size == 1) { + val index = indices.head + indexedWebs.update(index, indexedWebs(index).combine(web)) + web.defPCs.foreach(defPCToWebIndex.update(_, index)) + } else { + val newIndex = indices.head + val originalWebs = indices.map(indexedWebs) + indexedWebs.update(newIndex, originalWebs.reduce(_.combine(_)).combine(web)) + indices.tail.foreach(indexedWebs.update(_, null)) + originalWebs.foreach(_.defPCs.foreach(defPCToWebIndex.update(_, newIndex))) + web.defPCs.foreach(defPCToWebIndex.update(_, newIndex)) + } + } else { + val newIndex = indexedWebs.length + indexedWebs.append(web) + web.defPCs.foreach(defPCToWebIndex.update(_, newIndex)) + } + } + + val startMap = indexedWebs.filter(_ != null) + .map { web: PDUWeb => + val defPC = web.defPCs.toList.min + + if (defPC >= 0) { + (web, StringTreeInvalidElement) + } else if (defPC < -1 && defPC > ImmediateVMExceptionsOriginOffset) { + (web, StringTreeParameter.forParameterPC(defPC)) + } else { + // IMPROVE interpret "this" (parameter pc -1) with reference to String and StringBuilder classes + (web, StringInterpreter.failureTree) + } + }.toMap + _startEnv = StringTreeEnvironment(startMap) + pcToWebChangeMapping.mapValuesInPlace((_, _) => false) + } + _startEnv + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/StructuralAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/StructuralAnalysis.scala new file mode 100644 index 0000000000..27110fd72d --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/StructuralAnalysis.scala @@ -0,0 +1,456 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package flowanalysis + +import scala.collection.mutable + +import org.opalj.graphs.DominatorTree + +import scalax.collection.GraphTraversal.DepthFirst +import scalax.collection.GraphTraversal.Parameters +import scalax.collection.OneOrMore +import scalax.collection.OuterEdge +import scalax.collection.edges.DiEdge +import scalax.collection.generic.Edge +import scalax.collection.hyperedges.DiHyperEdge +import scalax.collection.immutable.Graph +import scalax.collection.mutable.{Graph => MutableGraph} + +/** + * An algorithm that identifies several different types of flow regions in a given flow graph and reduces them to a + * single node iteratively. The algorithm terminates either when a single node is left in the flow graph or such a state + * could not be reached after [[maxIterations]]. + * + * On termination, the [[analyze]] function returns: + *
    + *
  1. + * The reduced flow graph, a single node equal to the root node of the control tree. + *
  2. + *
  3. + * A super flow graph as a combination of the given source flow graph and the control tree. For each + * node contained in the control tree, the super flow graph contains the node itself and edges to its children + * as referenced the control tree. However, its children are still connected with edges as contained in the + * source flow graph. + *
    + * This representation eases traversal for data flow analysis such as by [[DataFlowAnalysis]]. + *
  4. + *
  5. + * The control tree, as a hierarchic representation of the control flow regions identified by the algorithm. + *
  6. + *
+ * + * This algorithm is adapted from Muchnick, S.S. (1997). Advanced Compiler Design and Implementation and optimized for + * performance. + * + * @see [[MethodStringFlowAnalysis]], [[FlowGraphNode]], [[DataFlowAnalysis]] + * + * @author Maximilian Rüsch + */ +object StructuralAnalysis { + + private final val maxIterations = 1000 + + private type MFlowGraph = MutableGraph[FlowGraphNode, DiEdge[FlowGraphNode]] + private type MControlTree = MutableGraph[FlowGraphNode, DiEdge[FlowGraphNode]] + private type MSuperFlowGraph = MutableGraph[FlowGraphNode, Edge[FlowGraphNode]] + + def analyze(initialGraph: FlowGraph, entry: FlowGraphNode): (FlowGraph, SuperFlowGraph, ControlTree) = { + val flowGraph: MFlowGraph = MutableGraph.from(initialGraph.edges.outerIterable) + val superFlowGraph: MSuperFlowGraph = + MutableGraph.from(initialGraph.edges.outerIterable).asInstanceOf[MSuperFlowGraph] + var currentEntry = entry + val controlTree: MControlTree = MutableGraph.empty[FlowGraphNode, DiEdge[FlowGraphNode]] + + var (immediateDominators, allDominators) = computeDominators(flowGraph, entry) + /** + * @return True when the given node n strictly dominates the node w. + */ + def strictlyDominates(n: FlowGraphNode, w: FlowGraphNode): Boolean = n != w && allDominators(w).contains(n) + + val knownPartOfNoCycle = mutable.Set.empty[FlowGraphNode] + def inCycle(n: FlowGraphNode): Boolean = { + if (knownPartOfNoCycle.contains(n)) { + false + } else { + // IMPROVE if no cycle is found, we can use the visitor of `findCycleContaining` to identify more nodes + // that are known to not be part of a cycle and add them to `knownPartOfNoCycle`. + val cycleOpt = flowGraph.findCycleContaining(flowGraph.get(n)) + if (cycleOpt.isDefined) { + true + } else { + knownPartOfNoCycle.add(n) + false + } + } + } + + var iterations = 0 + while (flowGraph.order > 1 && iterations < maxIterations) { + // Find post order depth first traversal order for nodes + var postCtr = 1 + val post = mutable.ListBuffer.empty[FlowGraphNode] + + def replace(subNodes: Set[FlowGraphNode], entry: FlowGraphNode, regionType: RegionType): Unit = { + val newRegion = Region(regionType, subNodes.flatMap(_.nodeIds), entry) + + // Compact + // Note that adding the new region to the graph and superGraph is done anyways since we add edges later + val maxPost = post.indexOf(subNodes.maxBy(post.indexOf)) + post(maxPost) = newRegion + // Removing old regions from the graph is done later + post.filterInPlace(r => !subNodes.contains(r)) + postCtr = post.indexOf(newRegion) + + if (subNodes.forall(knownPartOfNoCycle.contains)) { + knownPartOfNoCycle.add(newRegion) + } + knownPartOfNoCycle.subtractAll(subNodes) + + // Replace edges + val incomingEdges = flowGraph.edges.filter { e => + !subNodes.contains(e.outer.source) && subNodes.contains(e.outer.target) + } + val outgoingEdges = flowGraph.edges.filter { e => + subNodes.contains(e.outer.source) && !subNodes.contains(e.outer.target) + } + + val newRegionEdges = incomingEdges.map { e => + OuterEdge[FlowGraphNode, DiEdge[FlowGraphNode]](DiEdge(e.outer.source, newRegion)) + }.concat(outgoingEdges.map { e => + OuterEdge[FlowGraphNode, DiEdge[FlowGraphNode]](DiEdge(newRegion, e.outer.target)) + }) + flowGraph.addAll(newRegionEdges) + flowGraph.removeAll(subNodes, Set.empty) + + superFlowGraph.addAll(newRegionEdges) + superFlowGraph.removeAll { + incomingEdges.concat(outgoingEdges).map(e => DiEdge(e.outer.source, e.outer.target)) + .concat(Seq(DiHyperEdge(OneOrMore(newRegion), OneOrMore.from(subNodes).get))) + } + + // Update dominator data + val commonDominators = subNodes.map(allDominators).reduce(_.intersect(_)) + allDominators.subtractAll(subNodes).update(newRegion, commonDominators) + allDominators = allDominators.map(kv => + ( + kv._1, { + val index = kv._2.indexWhere(subNodes.contains) + if (index != -1) + kv._2.patch(index, Seq(newRegion), kv._2.lastIndexWhere(subNodes.contains) - index + 1) + else + kv._2 + } + ) + ) + immediateDominators = allDominators.map(kv => (kv._1, kv._2.head)) + + // Update remaining graph state + controlTree.addAll(subNodes.map(node => + OuterEdge[FlowGraphNode, DiEdge[FlowGraphNode]](DiEdge(newRegion, node)) + )) + if (subNodes.contains(currentEntry)) { + currentEntry = newRegion + } + } + + // Determine post-order depth-first traversal in the given graph + val ordering = flowGraph.NodeOrdering((in1, in2) => in1.compare(in2)) + flowGraph.innerNodeDownUpTraverser( + flowGraph.get(currentEntry), + Parameters(DepthFirst), + ordering = ordering + ).foreach { + case (down, in) if !down => post.append(in.outer) + case _ => + } + + while (flowGraph.order > 1 && postCtr < post.size) { + var n = post(postCtr) + + val gPostMap = + post.reverse.zipWithIndex.map(ni => + (flowGraph.get(ni._1).asInstanceOf[MFlowGraph#NodeT], ni._2) + ).toMap + val (newStartingNode, acyclicRegionOpt) = + locateAcyclicRegion[FlowGraphNode, MFlowGraph](flowGraph, gPostMap, allDominators)(n) + n = newStartingNode + if (acyclicRegionOpt.isDefined) { + val (arType, nodes, entry) = acyclicRegionOpt.get + replace(nodes, entry, arType) + } else if (inCycle(n)) { + var reachUnder = Set(n) + for { + m <- flowGraph.nodes.outerIterator + if m != n + innerM = controlTree.find(m) + if innerM.isEmpty || !innerM.get.hasPredecessors + if StructuralAnalysis.pathBack[FlowGraphNode, MFlowGraph](flowGraph, strictlyDominates)(m, n) + } { + reachUnder = reachUnder.incl(m) + } + + val cyclicRegionOpt = locateCyclicRegion[FlowGraphNode, MFlowGraph](flowGraph, n, reachUnder) + if (cyclicRegionOpt.isDefined) { + val (crType, nodes, entry) = cyclicRegionOpt.get + replace(nodes, entry, crType) + } else { + postCtr += 1 + } + } else { + postCtr += 1 + } + } + + iterations += 1 + } + + if (iterations >= maxIterations) { + throw new IllegalStateException(s"Could not reduce tree in $maxIterations iterations!") + } + + ( + Graph.from(flowGraph.edges.outerIterable), + Graph.from(superFlowGraph.edges.outerIterable), + Graph.from(controlTree.edges.outerIterable) + ) + } + + /** + * Computes the immediate and global dominators for each of the nodes in the given graph. + * + * @param graph The graph to compute dominators for. + * @param entry The entry node to the graph under analysis. + * @return (A map for immediate dominators by graph node, A map for all dominators of a graph node by graph node) + */ + private def computeDominators[A, G <: MutableGraph[A, DiEdge[A]]]( + graph: G, + entry: A + ): (mutable.Map[A, A], mutable.Map[A, Seq[A]]) = { + val indexedNodes = graph.nodes.toIndexedSeq + val indexOf = indexedNodes.zipWithIndex.toMap + val domTree = DominatorTree( + indexOf(graph.get(entry)), + graph.get(entry).hasPredecessors, + index => { f => indexedNodes(index).diSuccessors.foreach(ds => f(indexOf(ds))) }, + index => { f => indexedNodes(index).diPredecessors.foreach(ds => f(indexOf(ds))) }, + indexedNodes.size - 1 + ) + val outerIndexedNodes = indexedNodes.map(_.outer) + val immediateDominators = mutable.Map.from { + domTree.immediateDominators.zipWithIndex.map(iDomWithIndex => { + (outerIndexedNodes(iDomWithIndex._2), outerIndexedNodes(iDomWithIndex._1)) + }) + } + immediateDominators.update(entry, entry) + + def getAllDominators(n: A): Seq[A] = { + val builder = Seq.newBuilder[A] + var c = n + while (c != entry) { + builder.addOne(c) + c = immediateDominators(c) + } + builder.addOne(entry) + builder.result() + } + val allDominators = immediateDominators.map(kv => (kv._1, getAllDominators(kv._2))) + + (immediateDominators, allDominators) + } + + /** + * Determines if a path exists from the given node m to some other node k that does not contain the node n AND an + * edge k -> n exists that is a back edge in the given graph. The latter is realized by using predecessors to find + * eligible edges and using a strict domination predicate to determine a back edge from it. + * + * @param graph The graph under analysis. + * @param strictlyDominates Predicate if a given first node strictly dominates the given second nodes in the given graph. + * @param m The node that forms the starting node of the path. + * @param n The node that forms the ending node of the path. + * @return True if there is path back from m to n over some intermediate node k where m -> k does not contain n. + */ + private def pathBack[A, G <: MutableGraph[A, DiEdge[A]]](graph: G, strictlyDominates: (A, A) => Boolean)( + m: A, + n: A + ): Boolean = { + val innerN = graph.get(n) + val nonNFromMTraverser = graph.innerNodeTraverser(graph.get(m), subgraphNodes = _ != innerN) + val predecessorsOfN = innerN.diPredecessors + graph.nodes.exists { innerK => + innerK.outer != n && + predecessorsOfN.contains(innerK) && + strictlyDominates(n, innerK.outer) && + nonNFromMTraverser.pathTo(innerK).isDefined + } + } + + /** + * Identifies a candidate acyclic region if one can be found starting the identification at the given start node. + * If no region can be found, a new starting node is returned that can be used to resume searching for other region + * types. If a region can be found, all information needed to replace the contained nodes with a new region node in + * the graph is returned. + * + * @param graph The graph under analysis. + * @param postOrderTraversal A post order DFS traversal of the given graph as a mapping from node to its DF position. + * @param allDominators A dominator index as produced by [[computeDominators]]. + * @param startingNode The node to start locating the acyclic region at. + * @return The new starting node of a candidate region as well as an option of: 1. The type of the found acyclic + * region, 2. a set containing all region nodes and 3. the entry node to the region. + */ + private def locateAcyclicRegion[A <: FlowGraphNode, G <: MutableGraph[A, DiEdge[A]]]( + graph: G, + postOrderTraversal: Map[G#NodeT, Int], + allDominators: mutable.Map[A, Seq[A]] + )(startingNode: A): (A, Option[(AcyclicRegionType, Set[A], A)]) = { + var nSet = Set.empty[graph.NodeT] + var entry: graph.NodeT = graph.get(startingNode) + + // Expand nSet down + var n = graph.get(startingNode) + while ((n.outer == startingNode || n.diPredecessors.size == 1) && n.diSuccessors.size == 1) { + nSet += n + n = n.diSuccessors.head + } + if (n.diPredecessors.size == 1) { + nSet += n + } + + // Expand nSet up + n = graph.get(startingNode) + while (n.diPredecessors.size == 1 && (n.outer == startingNode || n.diSuccessors.size == 1)) { + nSet += n + entry = n + n = n.diPredecessors.head + } + if (n.diSuccessors.size == 1) { + nSet += n + entry = n + } + + def isAcyclic(nodes: Set[graph.NodeT]): Boolean = { + nodes.forall { node => + val postOrderIndex = postOrderTraversal(node) + node.diSuccessors.forall { successor => + !nodes.contains(successor) || postOrderTraversal(successor) >= postOrderIndex + } + } + } + + def locateProperAcyclicInterval: Option[AcyclicRegionType] = { + val dominatedNodes = allDominators.filter(_._2.contains(n.outer)).map(kv => graph.get(kv._1)).toSet ++ Set(n) + if (dominatedNodes.size == 1 || + !isAcyclic(dominatedNodes) || + // Check if no dominated node is reached from a non-dominated node + !dominatedNodes.excl(n).forall(_.diPredecessors.subsetOf(dominatedNodes)) || + // Check if all dominated nodes agree on a single successor outside the set (if it exists) + dominatedNodes.flatMap(node => node.diSuccessors.diff(dominatedNodes)).size > 1 + ) { + None + } else { + nSet = dominatedNodes + entry = n + + Some(Proper) + } + } + + val newDirectSuccessors = n.diSuccessors + val rType = if (nSet.size > 1) { + // Condition is added to ensure chosen bb does not contain any self loops or other cyclic regions + // IMPROVE weaken to allow back edges from the "last" nSet member to the first to enable reductions to self loops + if (isAcyclic(nSet)) + Some(Block) + else + None + } else if (newDirectSuccessors.size == 2) { + val m = newDirectSuccessors.head + val k = newDirectSuccessors.tail.head + if (m.diSuccessors.headOption == k.diSuccessors.headOption + && m.diSuccessors.size == 1 + && m.diPredecessors.size == 1 + && k.diPredecessors.size == 1 + ) { + nSet = Set(n, m, k) + entry = n + Some(IfThenElse) + } else if (( + m.diSuccessors.size == 1 + && m.diSuccessors.head == k + && m.diPredecessors.size == 1 + && k.diPredecessors.size == 2 + ) || ( + k.diSuccessors.size == 1 + && k.diSuccessors.head == m + && k.diPredecessors.size == 1 + && m.diPredecessors.size == 2 + ) + ) { + nSet = Set(n, m, k) + entry = n + Some(IfThen) + } else { + locateProperAcyclicInterval + } + } else if (newDirectSuccessors.size > 2) { + locateProperAcyclicInterval + } else { + None + } + + (n.outer, rType.map((_, nSet.map(_.outer), entry.outer))) + } + + /** + * Identifies a candidate cyclic region if one can be found starting the identification at the given start node. + * + * @param graph The graph under analysis. + * @param startingNode The node to start identifying cyclic regions at. + * @param reachUnder A set of all nodes that can be reached starting from the given starting node that have a path + * back to the given start node. + * @return An option of: 1. The cyclic region type, 2. the nodes contained in the region and 3. the entry node of + * the region. + * + * @note This implementation does not yet support improper regions and their reduction and will throw upon their + * detection. + */ + private def locateCyclicRegion[A, G <: MutableGraph[A, DiEdge[A]]]( + graph: G, + startingNode: A, + reachUnder: Set[A] + ): Option[(CyclicRegionType, Set[A], A)] = { + if (reachUnder.size == 1) { + return if (graph.find(DiEdge(startingNode, startingNode)).isDefined) + Some((SelfLoop, reachUnder, reachUnder.head)) + else None + } + + if (reachUnder.exists(m => graph.get(startingNode).pathTo(graph.get(m)).isEmpty)) { + // IMPROVE reliably detect size of improper regions and reduce + throw new IllegalStateException("This implementation of structural analysis cannot handle improper regions!") + } + + val m = reachUnder.excl(startingNode).head + if (graph.get(startingNode).diPredecessors.size == 2 + && graph.get(startingNode).diSuccessors.size == 2 + && graph.get(m).diPredecessors.size == 1 + && graph.get(m).diSuccessors.size == 1 + ) { + Some((WhileLoop, reachUnder, startingNode)) + } else { + val enteringNodes = + reachUnder.filter(graph.get(_).diPredecessors.exists(dp => !reachUnder.contains(dp.outer))) + + if (enteringNodes.size > 1) { + throw new IllegalStateException("Found more than one entering node for a natural loop!") + } else if (enteringNodes.isEmpty) { + throw new IllegalStateException("Found no entering node for a natural loop!") + } + + Some((NaturalLoop, reachUnder, enteringNodes.head)) + } + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/package.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/package.scala new file mode 100644 index 0000000000..9208912ace --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/flowanalysis/package.scala @@ -0,0 +1,174 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string + +import org.opalj.br.cfg.BasicBlock +import org.opalj.br.cfg.CFG + +import scalax.collection.edges.DiEdge +import scalax.collection.generic.Edge +import scalax.collection.immutable.Graph +import scalax.collection.immutable.TypedGraphFactory +import scalax.collection.io.dot.DotAttr +import scalax.collection.io.dot.DotAttrStmt +import scalax.collection.io.dot.DotEdgeStmt +import scalax.collection.io.dot.DotGraph +import scalax.collection.io.dot.DotNodeStmt +import scalax.collection.io.dot.DotRootGraph +import scalax.collection.io.dot.Elem +import scalax.collection.io.dot.Graph2DotExport +import scalax.collection.io.dot.Id +import scalax.collection.io.dot.NodeId + +/** + * @author Maximilian Rüsch + */ +package object flowanalysis { + + type ControlTree = Graph[FlowGraphNode, DiEdge[FlowGraphNode]] + type FlowGraph = Graph[FlowGraphNode, DiEdge[FlowGraphNode]] + type SuperFlowGraph = Graph[FlowGraphNode, Edge[FlowGraphNode]] + + object FlowGraph extends TypedGraphFactory[FlowGraphNode, DiEdge[FlowGraphNode]] { + + /** + * The entry point of all flow graphs. + * + * @see [[GlobalEntry]] + */ + val entry: FlowGraphNode = GlobalEntry + + private def mapInstrIndexToPC[V <: Var[V]](cfg: CFG[Stmt[V], TACStmts[V]])(index: Int): Int = { + if (index >= 0) cfg.code.instructions(index).pc + else index + } + + /** + * Converts a given CFG to a flow graph with additional global entry and exit nodes. + * + * @see [[FlowGraphNode]] + * + * @param cfg The CFG to convert to a flow graph. + * @return The flow graph obtained from the CFG. + */ + def apply[V <: Var[V]](cfg: CFG[Stmt[V], TACStmts[V]]): FlowGraph = { + val toPC = mapInstrIndexToPC(cfg) _ + + val edges = cfg.allNodes.flatMap { + case bb: BasicBlock => + val firstNode = Statement(toPC(bb.startPC)) + var currentEdges = Seq.empty[DiEdge[FlowGraphNode]] + if (bb.startPC != bb.endPC) { + Range.inclusive(bb.startPC, bb.endPC).tail.foreach { instrIndex => + currentEdges :+= DiEdge( + currentEdges.lastOption.map(_.target).getOrElse(firstNode), + Statement(toPC(instrIndex)) + ) + } + } + + val lastNode = if (currentEdges.nonEmpty) currentEdges.last.target + else firstNode + currentEdges ++ bb.successors.map(s => DiEdge(lastNode, Statement(toPC(s.nodeId)))) + case n => + n.successors.map(s => DiEdge(Statement(toPC(n.nodeId)), Statement(toPC(s.nodeId)))) + }.toSet + val g = Graph.from(edges + DiEdge(entry, entryFromCFG(cfg))) + + val normalReturnNode = Statement(cfg.normalReturnNode.nodeId) + val abnormalReturnNode = Statement(cfg.abnormalReturnNode.nodeId) + val hasNormalReturn = cfg.normalReturnNode.predecessors.nonEmpty + val hasAbnormalReturn = cfg.abnormalReturnNode.predecessors.nonEmpty + + (hasNormalReturn, hasAbnormalReturn) match { + case (true, true) => + g.incl(DiEdge(normalReturnNode, GlobalExit)).incl(DiEdge(abnormalReturnNode, GlobalExit)) + + case (true, false) => + g.excl(abnormalReturnNode) + + case (false, true) => + g.excl(normalReturnNode) + + case _ => + throw new IllegalStateException( + "Cannot transform a CFG with neither normal nor abnormal return edges!" + ) + } + } + + private[this] def entryFromCFG[V <: Var[V]](cfg: CFG[Stmt[V], TACStmts[V]]): Statement = + Statement(mapInstrIndexToPC(cfg)(cfg.startBlock.nodeId)) + + /** + * @param graph The graph consisting of flow graph nodes to convert to DOT format. + * @return A DOT string of the graph. + */ + def toDot[N <: FlowGraphNode, E <: Edge[N]](graph: Graph[N, E]): String = { + val root = DotRootGraph( + directed = true, + id = Some(Id("MyDot")), + attrStmts = List(DotAttrStmt(Elem.node, List(DotAttr(Id("shape"), Id("record"))))), + attrList = List(DotAttr(Id("attr_1"), Id(""""one"""")), DotAttr(Id("attr_2"), Id(""))) + ) + + def edgeTransformer(innerEdge: Graph[N, E]#EdgeT): Option[(DotGraph, DotEdgeStmt)] = { + val edge = innerEdge.outer + Some( + ( + root, + DotEdgeStmt(NodeId(edge.sources.head.toString), NodeId(edge.targets.head.toString)) + ) + ) + } + + def hEdgeTransformer(innerHEdge: Graph[N, E]#EdgeT): Iterable[(DotGraph, DotEdgeStmt)] = { + val color = DotAttr(Id("color"), Id(s""""#%06x"""".format(scala.util.Random.nextInt(1 << 24)))) + + innerHEdge.outer.targets.toList map (target => + ( + root, + DotEdgeStmt( + NodeId(innerHEdge.outer.sources.head.toString), + NodeId(target.toString), + Seq(color) + ) + ) + ) + } + + def nodeTransformer(innerNode: Graph[N, E]#NodeT): Option[(DotGraph, DotNodeStmt)] = { + val node = innerNode.outer + val attributes = if (node.nodeIds.size == 1) Seq.empty + else Seq( + DotAttr(Id("style"), Id("filled")), + DotAttr( + Id("fillcolor"), + node match { + case Region(_: AcyclicRegionType, _, _) => Id(""""green"""") + case Region(_: CyclicRegionType, _, _) => Id(""""purple"""") + case _ => Id(""""white"""") + } + ) + ) + Some( + ( + root, + DotNodeStmt(NodeId(node.toString), attributes) + ) + ) + } + + graph.toDot( + root, + edgeTransformer, + hEdgeTransformer = Some(hEdgeTransformer), + cNodeTransformer = Some(nodeTransformer), + iNodeTransformer = Some(nodeTransformer) + ) + } + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationHandler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationHandler.scala new file mode 100644 index 0000000000..e361d14ebd --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationHandler.scala @@ -0,0 +1,81 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package interpretation + +import org.opalj.br.Method +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.fpcf.FinalEP +import org.opalj.fpcf.InterimResult +import org.opalj.fpcf.ProperPropertyComputationResult +import org.opalj.fpcf.SomeEPS +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty + +/** + * Processes expressions that are relevant in order to determine which value(s) the string value at a given def site + * might have. Produces string flow functions that transform a given string state during data flow analysis. + * + * @note [[InterpretationHandler]]s of any level may use [[StringInterpreter]]s from their level or any level below. + * + * @see [[StringFlowFunctionProperty]], [[org.opalj.tac.fpcf.analyses.string.flowanalysis.DataFlowAnalysis]] + * + * @author Maximilian Rüsch + */ +abstract class InterpretationHandler extends FPCFAnalysis with StringAnalysisConfig { + + def analyze(entity: MethodPC): ProperPropertyComputationResult = { + val tacaiEOptP = ps(entity.dm.definedMethod, TACAI.key) + implicit val state: InterpretationState = InterpretationState(entity.pc, entity.dm, tacaiEOptP) + + if (tacaiEOptP.isRefinable) { + InterimResult( + InterpretationHandler.getEntity, + StringFlowFunctionProperty.lb, + StringFlowFunctionProperty.ub, + Set(state.tacDependee), + continuation(state) + ) + } else if (tacaiEOptP.ub.tac.isEmpty) { + // No TAC available, e.g., because the method has no body + StringInterpreter.computeFinalResult(StringFlowFunctionProperty.constForAll(StringInterpreter.failureTree)) + } else { + processStatementForState + } + } + + private def continuation(state: InterpretationState)(eps: SomeEPS): ProperPropertyComputationResult = { + eps match { + case FinalEP(_, _) if eps.pk.equals(TACAI.key) => + state.tacDependee = eps.asInstanceOf[FinalEP[Method, TACAI]] + processStatementForState(state) + + case _ => + InterimResult.forUB( + InterpretationHandler.getEntity(state), + StringFlowFunctionProperty.ub, + Set(state.tacDependee), + continuation(state) + ) + } + } + + private def processStatementForState(implicit state: InterpretationState): ProperPropertyComputationResult = { + val defSiteOpt = valueOriginOfPC(state.pc, state.tac.pcToIndex); + if (defSiteOpt.isEmpty) { + throw new IllegalArgumentException(s"Obtained a pc that does not represent a definition site: ${state.pc}") + } + + processStatement(state)(state.tac.stmts(defSiteOpt.get)) + } + + protected def processStatement(implicit state: InterpretationState): Stmt[V] => ProperPropertyComputationResult +} + +object InterpretationHandler { + + def getEntity(implicit state: InterpretationState): MethodPC = MethodPC(state.pc, state.dm) +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationState.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationState.scala new file mode 100644 index 0000000000..85f41cdfd7 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/InterpretationState.scala @@ -0,0 +1,32 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package interpretation + +import org.opalj.br.DefinedMethod +import org.opalj.br.Method +import org.opalj.fpcf.EOptionP +import org.opalj.tac.fpcf.properties.TACAI + +/** + * The state for the FPCF analysis responsible for interpreting the statement at the given PC of the given method and + * obtaining its string flow information. + * + * @see [[InterpretationHandler]], [[StringInterpreter]] + * + * @param pc The PC of the statement under analysis. + * @param dm The method of the statement under analysis. + * @param tacDependee The initial TACAI dependee of the method under analysis. + */ +case class InterpretationState(pc: Int, dm: DefinedMethod, var tacDependee: EOptionP[Method, TACAI]) { + + def tac: TAC = { + if (tacDependee.hasUBP && tacDependee.ub.tac.isDefined) + tacDependee.ub.tac.get + else + throw new IllegalStateException("Cannot get a TAC from a TACAI with no or empty upper bound!") + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/StringFlowAnalysisScheduler.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/StringFlowAnalysisScheduler.scala new file mode 100644 index 0000000000..5bd4f4c63d --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/interpretation/StringFlowAnalysisScheduler.scala @@ -0,0 +1,54 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package interpretation + +import org.opalj.br.analyses.DeclaredMethodsKey +import org.opalj.br.analyses.ProjectInformationKeys +import org.opalj.br.analyses.SomeProject +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.FPCFAnalysisScheduler +import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler +import org.opalj.fpcf.PropertyBounds +import org.opalj.fpcf.PropertyStore +import org.opalj.tac.fpcf.properties.TACAI +import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty + +/** + * A general scheduler trait for the different string flow analysis levels. + * + * @see [[InterpretationHandler]] + * + * @author Maximilian Rüsch + */ +sealed trait StringFlowAnalysisScheduler extends FPCFAnalysisScheduler { + + final def derivedProperty: PropertyBounds = PropertyBounds.lub(StringFlowFunctionProperty) + + override def uses: Set[PropertyBounds] = PropertyBounds.ubs(TACAI) + + override final type InitializationData = InterpretationHandler + + override def beforeSchedule(p: SomeProject, ps: PropertyStore): Unit = {} + + override def afterPhaseScheduling(ps: PropertyStore, analysis: FPCFAnalysis): Unit = {} + + override def afterPhaseCompletion(p: SomeProject, ps: PropertyStore, analysis: FPCFAnalysis): Unit = {} +} + +trait LazyStringFlowAnalysis + extends StringFlowAnalysisScheduler with FPCFLazyAnalysisScheduler { + + override def register(p: SomeProject, ps: PropertyStore, initData: InitializationData): FPCFAnalysis = { + ps.registerLazyPropertyComputation(StringFlowFunctionProperty.key, initData.analyze) + + initData + } + + override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) + + override def requiredProjectInformation: ProjectInformationKeys = Seq(DeclaredMethodsKey) +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/L0StringAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/L0StringAnalysis.scala new file mode 100644 index 0000000000..bf3d75c2d7 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/L0StringAnalysis.scala @@ -0,0 +1,32 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package l0 + +import org.opalj.br.analyses.SomeProject +import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler +import org.opalj.fpcf.PropertyStore +import org.opalj.tac.fpcf.analyses.string.flowanalysis.LazyMethodStringFlowAnalysis +import org.opalj.tac.fpcf.analyses.string.interpretation.LazyStringFlowAnalysis +import org.opalj.tac.fpcf.analyses.string.l0.interpretation.L0InterpretationHandler + +/** + * @see [[L0InterpretationHandler]] + * @author Maximilian Rüsch + */ +object LazyL0StringAnalysis { + + def allRequiredAnalyses: Seq[FPCFLazyAnalysisScheduler] = Seq( + LazyStringAnalysis, + LazyMethodStringFlowAnalysis, + LazyL0StringFlowAnalysis + ) +} + +object LazyL0StringFlowAnalysis extends LazyStringFlowAnalysis { + + override def init(p: SomeProject, ps: PropertyStore): InitializationData = L0InterpretationHandler(p) +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/BinaryExprInterpreter.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/BinaryExprInterpreter.scala new file mode 100644 index 0000000000..75f0b279e9 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/string/l0/interpretation/BinaryExprInterpreter.scala @@ -0,0 +1,56 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package string +package l0 +package interpretation + +import org.opalj.br.ComputationalTypeFloat +import org.opalj.br.ComputationalTypeInt +import org.opalj.br.fpcf.properties.string.StringTreeDynamicFloat +import org.opalj.br.fpcf.properties.string.StringTreeDynamicInt +import org.opalj.br.fpcf.properties.string.StringTreeNode +import org.opalj.fpcf.ProperPropertyComputationResult +import org.opalj.tac.fpcf.analyses.string.interpretation.InterpretationState +import org.opalj.tac.fpcf.properties.string.StringFlowFunctionProperty + +/** + * Interprets the given assignment statement containing a [[BinaryExpr]] by determining its return type and using the + * appropriate dynamic [[StringTreeNode]] in high soundness mode. In low soundness mode, no string value can be + * determined. + * + * @author Maximilian Rüsch + */ +case class BinaryExprInterpreter()( + implicit val highSoundness: Boolean +) extends AssignmentBasedStringInterpreter { + + override type E = BinaryExpr[V] + + /** + * Currently, this implementation supports the interpretation of the following binary expressions: + *