
org.opalj.br.analyses.ProjectLike.scala Maven / Gradle / Ivy
The newest version!
/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package br
package analyses
import scala.collection.{mutable, Map => SomeMap, Set => SomeSet}
import com.typesafe.config.Config
import org.opalj.log.LogContext
import org.opalj.log.OPALLogger
import org.opalj.log.OPALLogger.error
import org.opalj.log.OPALLogger.info
import org.opalj.collection.immutable.UIDSet
import org.opalj.bi.Java11MajorVersion
import org.opalj.bi.Java1MajorVersion
import org.opalj.br.instructions.FieldAccess
import org.opalj.br.instructions.INVOKEINTERFACE
import org.opalj.br.instructions.INVOKESPECIAL
import org.opalj.br.instructions.INVOKESTATIC
import org.opalj.br.instructions.INVOKEVIRTUAL
import org.opalj.br.instructions.NonVirtualMethodInvocationInstruction
import org.opalj.br.MethodDescriptor.SignaturePolymorphicMethodBoolean
import org.opalj.br.MethodDescriptor.SignaturePolymorphicMethodObject
import org.opalj.br.MethodDescriptor.SignaturePolymorphicMethodVoid
import scala.collection.immutable.ArraySeq
import control.find
/**
* Enables project wide lookups of methods and fields as required to determine the target(s) of an
* invoke or field access instruction.
*
* @note The current implementation is based on the '''correct project assumption''';
* i.e., if the bytecode of the project as a whole is not valid, the result is generally
* undefined.
* Just one example of a violation of the assumption would be,
* if we have two interfaces which define a non-abstract
* method with the same signature and both interfaces are implemented by a third
* interface which does not override these methods. In this case the result of a
* `resolveMethodReference` is not defined, because the code base as a whole is
* not valid.
*
* @author Michael Eichberg
*/
abstract class ProjectLike extends ClassFileRepository { project =>
private[this] final implicit val thisProjectLike: this.type = this
implicit val classHierarchy: ClassHierarchy
implicit val config: Config
protected[this] val allClassFiles: Iterable[ClassFile]
/**
* Returns the minimum version number of the JVM required to run the code of the project, i.e.,
* the maximum class file major version number of any class file in the project.
*/
def requiredJVMVersion: Int = {
if (allClassFiles.isEmpty) Java1MajorVersion
else allClassFiles.maxBy(_.version._2).version._2
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//
//
// RESOLVING FIELD REFERENCES
//
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/**
* @see [[#resolveFieldReference(declaringClassFile:*]]
*/
final def resolveFieldReference(
declaringClassType: ObjectType,
fieldName: String,
fieldType: FieldType
): Option[Field] = {
// for more details see JVM 7/8 Spec. Section 5.4.3.2
project.classFile(declaringClassType) flatMap { classFile =>
resolveFieldReference(classFile, fieldName, fieldType)
}
}
/**
* Resolves a symbolic reference to a field. Basically, the search starts with
* the given class `c` and then continues with `c`'s superinterfaces before the
* search is continued with `c`'s superclass (as prescribed by the JVM specification
* for the resolution of unresolved symbolic references).
*
* Resolving a symbolic reference is particularly required to, e.g., get a field's
* annotations or to get a field's value (if it is `static`, `final` and has a
* constant value).
*
* @note This implementation does not check for `IllegalAccessError`. This check
* needs to be done by the caller. The same applies for the check that the
* field is non-static if get-/putfield is used and static if a get-/putstatic is
* used to access the field. In the latter case the JVM would throw a
* `LinkingException`.
* Furthermore, if the field cannot be found, it is the responsibility of the
* caller to handle that situation.
* @note Resolution is final. I.e., either this algorithm has found the defining field
* or the field is not defined by one of the loaded classes. Searching for the
* field in subclasses is not meaningful as it is not possible to ''override''
* fields.
*
* @param declaringClassFile The class (or a superclass thereof) that is expected
* to define the specified field.
* @param fieldName The name of the field.
* @param fieldType The type of the field (the field descriptor).
*/
def resolveFieldReference(
declaringClassFile: ClassFile,
fieldName: String,
fieldType: FieldType
): Option[Field] = {
// for more details see JVM 7/8 Spec. Section 5.4.3.2
declaringClassFile findField (fieldName, fieldType) orElse {
declaringClassFile.interfaceTypes collectFirst { supertype =>
resolveFieldReference(supertype, fieldName, fieldType) match {
case Some(resolvedFieldReference) => resolvedFieldReference
}
} orElse {
declaringClassFile.superclassType flatMap { supertype =>
resolveFieldReference(supertype, fieldName, fieldType)
}
}
}
}
/**
* @see [[#resolveFieldReference(declaringClassFile:*]]
*/
final def resolveFieldReference(fieldAccess: FieldAccess): Option[Field] = {
resolveFieldReference(fieldAccess.declaringClass, fieldAccess.name, fieldAccess.fieldType)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//
//
// RESOLVING METHOD REFERENCES / LOCATING THE INVOKED METHOD(S)
//
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/**
* The class file of `java.lang.Object`, if available.
*/
val ObjectClassFile: Option[ClassFile]
/**
* The class file of `java.lang.invoke.MethodHandle`, if available.
*/
val MethodHandleClassFile: Option[ClassFile]
/**
* The class file of `java.lang.invoke.VarHandle`, if available.
*/
val VarHandleClassFile: Option[ClassFile]
/**
* The set of all subtypes of `java.lang.invoke.MethodHandle`; in particular required to resolve
* signature polymorphic method calls.
*/
val MethodHandleSubtypes: SomeSet[ObjectType]
/**
* The set of all subtypes of `java.lang.invoke.VarHandle`; in particular required to resolve
* signature polymorphic method calls.
*/
val VarHandleSubtypes: SomeSet[ObjectType]
/**
* Stores for each non-private, non-initializer method the set of methods which override
* a specific method. If the given method is a concrete method, this method is also
* included in the set of `overridingMethods`.
*/
protected[this] val overridingMethods: SomeMap[Method, SomeSet[Method]]
/**
* Returns the set of methods which directly override the given method. Note that
* `overriddenBy` is not context aware. I.e., if a given method `m` is an interface
* method, then it may happen that we have an implementation of that method
* in a class which is inherited from a superclass which is not a subtype of the
* interface. That method - since it is not defined by a subtype of the interface -
* would not be included in the returned set. An example is shown next:
* {{{
* class X { void m(){ System.out.println("X.m"); }
* interface Y { default void m(){ System.out.println("Y.m"); }
* class Z extends X implements Y {
* // Z inherits m() from X; hence, X.m() (in this context) "overrides" Y.m(), but is not
* // returned by this function. To also identify X.m() you have to combine the results
* // of overridenBy and instanceMethods(!).
* }
* }}}
*/
def overriddenBy(m: Method): SomeSet[Method] = {
assert(!m.isPrivate, s"private methods $m cannot be overridden")
assert(!m.isStatic, s"static methods $m cannot be overridden")
assert(!m.isInitializer, s"initializers $m cannot be overridden")
overridingMethods.getOrElse(m, Set.empty)
}
/**
* Returns the set of all non-private, non-abstract, non-static methods that are not
* initializers and which are potentially callable by clients when we have an object that
* has the specified type and a method is called using
* [[org.opalj.br.instructions.INVOKEINTERFACE]], [[org.opalj.br.instructions.INVOKEVIRTUAL]] or
* [[org.opalj.br.instructions.INVOKEDYNAMIC]].
*
* The array of methods is sorted using [[MethodDeclarationContextOrdering]] to
* enable fast look-up of the target method. (See [[MethodDeclarationContext]]'s
* `compareAccessibilityAware` method for further details.)
*/
protected[this] val instanceMethods: SomeMap[ObjectType, ArraySeq[MethodDeclarationContext]]
/**
* Returns the nest host (see JVM 11 Spec. 5.4.4) for the given type, if explicitly given. For
* classes without an explicit NestHost or NestMembers attribute, the type itself is the nest
* host, but this is NOT recorded in this map.
*/
val nests: SomeMap[ObjectType, ObjectType]
/**
* Tests if the given method belongs to the interface of an '''object''' identified by
* the given `objectType`.
* I.e., returns `true` if a virtual method call, where the receiver type is known
* to have the given `objectType`, would lead to the direct invocation of the given `method`.
* The given method can be an inherited method, but it will never return `Yes` if
* the given method is overridden by `objectType` or a supertype of it which is a
* sub type of the declaring type of `method`.
*
* @note The computation is based on the computed set of [[instanceMethods]] and generally
* requires at most O(n log n) steps where n is the number of callable instance
* methods of the given object type; the class hierarchy is not traversed.
*/
def hasVirtualMethod(objectType: ObjectType, method: Method): Answer = {
// ... instanceMethods: Map[ObjectType, Array[MethodDeclarationContext]]
val definedMethodsOption = instanceMethods.get(objectType)
if (definedMethodsOption.isEmpty) {
return Unknown;
}
val definedMethods: ArraySeq[MethodDeclarationContext] = definedMethodsOption.get
val declaringPackageName = method.classFile.thisType.packageName
val result: Option[MethodDeclarationContext] = find(definedMethods) { definedMethodContext =>
val definedMethod = definedMethodContext.method
if (definedMethod eq method)
0
else {
val methodComparison = definedMethod compare method
if (methodComparison == 0) {
if (definedMethod.isPrivate) {
// If there is a matching private method, the given method could still be
// invoked by a virtual call for a supertype
return hasVirtualMethod(
classFile(objectType).get.superclassType.get,
method
);
} else {
// We may have multiple methods with the same signature, but which belong
// to different packages!
definedMethodContext.packageName compare declaringPackageName
}
} else
methodComparison
}
}
Answer(result)
}
/**
* Looks up the method (declaration context) which is accessible/callable by an
* [[org.opalj.br.instructions.INVOKEVIRTUAL]] or [[org.opalj.br.instructions.INVOKEINTERFACE]]
* call which was done by a method belonging to `callingContextType`.
* The `callingContextType` is only relevant in case the target method has default visibility;
* in this case it is checked whether the caller belongs to the same context.
*
* @note This method uses the pre-computed information about instance methods and,
* therefore, does not require a type hierarchy based lookup.
*
* @note It supports the lookup of polymorphic methods.
*
* @return [[Success]] if the method is found; [[Empty$]] if the method cannot be found and
* [[Failure$]] if the method cannot be found because the project is
* definitively inconsistent. `Failure$` is used on a best-effort basis.
*/
def lookupVirtualMethod(
callingContextType: ObjectType,
receiverType: ObjectType,
name: String,
descriptor: MethodDescriptor
): Result[MethodDeclarationContext] = {
def lookupSignaturePolymorphicMethod(descriptor: MethodDescriptor): Result[MethodDeclarationContext] = {
lookupVirtualMethod(
callingContextType,
if (MethodHandleSubtypes.contains(receiverType)) ObjectType.MethodHandle
else ObjectType.VarHandle,
name,
descriptor
) match {
case r @ Success(mdc) if mdc.method.isNativeAndVarargs => r
case _ => Empty
}
}
val definedMethodsOption = instanceMethods.get(receiverType)
if (definedMethodsOption.isEmpty) {
return Empty;
}
find(definedMethodsOption.get) { mdc =>
mdc.compareAccessibilityAware(callingContextType.packageName, name, descriptor)
} match {
case Some(mdc) => Success(mdc)
case None =>
// we have to avoid endless recursion if we can't find the target method
if ((MethodHandleSubtypes.contains(receiverType) ||
VarHandleSubtypes.contains(receiverType)) &&
!isSignaturePolymorphic(receiverType, descriptor)) {
// At least in Java 15 the signature polymorphic methods are not overloaded and
// it actually doesn't make sense to do so. Therefore we decided to only
// make this lookup if strictly required.
lookupSignaturePolymorphicMethod(SignaturePolymorphicMethodObject) match {
case Empty if VarHandleSubtypes.contains(receiverType) =>
lookupSignaturePolymorphicMethod(SignaturePolymorphicMethodVoid) match {
case Empty =>
lookupSignaturePolymorphicMethod(
SignaturePolymorphicMethodBoolean
)
case r => r
}
case r => r
}
} else {
Empty // here, we don't know if the project is incomplete or inconsistent
}
}
}
/* GENERAL NOTES
*
* (Accessibility checks are done by the JVM when the method is resolved; this is, however, not
* done by the following methods as it does not affect the search for the potential target
* methods.)
*
* Invokestatic => the resolved method is called (if it is accessible, static etc...)
* Invokespecial => the resolved (interface) method is called if it is a constructor
* call, or the resolved method belongs to the calling class (calls of
* private methods). Otherwise, the calling class' supertypes are
* searched for a method with the same signature up until the resolved
* method. When we search the type hierarchy upwards, we first search
* for a method defined by a superclass (unless the current class defines
* an interface type) before we search the interfaces. In the later
* case we compute the set of the maximally specific matching methods
* and select THE concrete one (it is an error if multiple concrete ones
* exist!)
* Invokevirtual => the resolved method is called, if the resolved method is signature
* polymorphic; in this case the concrete type of the method receiver
* is not relevant. Otherwise, the type of the receiver is used to
* start searching for the method that is to be invoked. That method
* however, has to override the resolved method (which is generally not
* the case if the method is private; and is only the case if the resolved
* method and the current type belong to the same package in case of
* a method with default visibility).
* Invokeinterface => the resolved interface method just defines an upper bound; the
* called method is determined as in case of invokevirtual; but
* signature polymorphic calls are not relevant
*/
/**
* Tries to resolve a method reference as specified by the JVM specification.
* I.e., the algorithm tries to find the class that actually declares the referenced
* method. Resolution of '''signature polymorphic''' method calls is also
* supported.
*
* This method can be used as the basis for the implementation of the semantics
* of the `invokeXXX` instructions. However, it does not check whether the resolved
* method can be accessed by the caller or if it is abstract. Additionally, it is still
* necessary that the caller makes a distinction between the statically
* (at compile time) identified declaring class and the dynamic type of the receiver
* in case of `invokevirtual` and `invokeinterface` instructions. I.e.,
* additional processing is necessary on the client side.
*
* @note This method just resolves a method reference. Additional checks,
* such as whether the resolved method is accessible, may be necessary.
*
* @param declaringClassType The type of the object that receives the method call. The
* type must be a class type and must not be an interface type.
* No check w.r.t. a potential `IncompatibleClassChangeError` is done
* by this method.
* @param forceLookupInSuperinterfacesOnFailure If true (default: false) the method tries
* to look up the method in a super interface if it can't find it in the available
* super classes.
* @return The resolved method `Some(`'''METHOD'''`)` or `None`.
* (To get the defining class file use the project's respective method.)
*/
def resolveMethodReference(
declaringClassType: ReferenceType,
name: String,
descriptor: MethodDescriptor,
forceLookupInSuperinterfacesOnFailure: Boolean = false
): Option[Method] = {
val receiverType =
if (declaringClassType.isArrayType) {
ObjectType.Object
} else {
declaringClassType.asObjectType
}
resolveClassMethodReference(receiverType, name, descriptor) match {
case Success(method) => Some(method)
case Empty if !forceLookupInSuperinterfacesOnFailure => None
case _ /*Failure | (Empty && lookupInSuperinterfacesOnFailure) */ =>
val superinterfaceTypes = classHierarchy.allSuperinterfacetypes(receiverType)
val (_, methods) =
findMaximallySpecificSuperinterfaceMethods(
superinterfaceTypes, name, descriptor,
analyzedSuperinterfaceTypes = UIDSet.empty[ObjectType]
)
// Either it is THE max. specific method or some "arbitrary" method.
// recall that we already give precedence to non-abstract
// methods in the find... methods
methods.headOption
}
}
/**
* Resolves a method reference to all possible methods. I.e., this is identical to
* `resolveMethodReference` or `resolveInterfaceMethodReference` for class and interface types
* respectively except for the case where there are multiple maximally specific interface
* methods in which case all of them are returned instead of only a single one.
*
* @param declaringClassType The type of the object that receives the method call. The type may
* be a class or interface type.
*
* @return The set of resolved methods; empty if the resolution fails, more than one if
* resolution finds several maximally specific interface methods - in the latter case
* it is not possible to call the method on objects of the declaring class type, but
* only on subclasses overriding the method uniquely.
*/
def resolveAllMethodReferences(
declaringClassType: ReferenceType,
name: String,
descriptor: MethodDescriptor
): Set[Method] = {
val receiverType =
if (declaringClassType.isArrayType) {
ObjectType.Object
} else {
declaringClassType.asObjectType
}
def lookupInObject(): Option[Method] = {
ObjectClassFile flatMap { classFile =>
classFile.findMethod(name, descriptor) filter { m => m.isPublic && !m.isStatic }
}
}
project.classFile(receiverType) match {
case Some(classFile) =>
val classMethod =
if (classFile.isInterfaceDeclaration)
Result(classFile.findMethod(name, descriptor) orElse lookupInObject())
else
resolveClassMethodReference(receiverType, name, descriptor)
classMethod match {
case Success(method) => Set(method)
case _ =>
val superinterfaces = classHierarchy.allSuperinterfacetypes(receiverType)
val (_, methods) = findMaximallySpecificSuperinterfaceMethods(
superinterfaces, name, descriptor, UIDSet.empty[ObjectType]
)
methods
}
case None => Set.empty
}
}
/**
* See [[#resolveMethodReference(declaringClassType:*]] for details.
*/
def resolveMethodReference(i: INVOKEVIRTUAL): Option[Method] = {
resolveMethodReference(i.declaringClass, i.name, i.methodDescriptor)
}
def resolveInterfaceMethodReference(
declaringClassType: ObjectType,
name: String,
descriptor: MethodDescriptor
): Option[Method] = {
def lookupInObject(): Option[Method] = {
ObjectClassFile flatMap { classFile =>
classFile.findMethod(name, descriptor) filter { m => m.isPublic && !m.isStatic }
}
}
project.classFile(declaringClassType) flatMap { classFile =>
assert(classFile.isInterfaceDeclaration)
classFile.findMethod(name, descriptor) orElse {
lookupInObject() orElse {
classHierarchy.superinterfaceTypes(declaringClassType) flatMap { superT =>
val (_, methods) = findMaximallySpecificSuperinterfaceMethods(
superT, name, descriptor, UIDSet.empty[ObjectType]
)
methods.headOption
}
}
}
}
}
/**
* See [[#resolveInterfaceMethodReference(declaringClassType:*]] for details.
*/
def resolveInterfaceMethodReference(i: INVOKEINTERFACE): Option[Method] = {
resolveInterfaceMethodReference(i.declaringClass, i.name, i.methodDescriptor)
}
/**
* Computes the set of maximally specific superinterface methods with the
* given name and descriptor.
*
* @note '''This method does not consider methods defined by `java.lang.Object`'''!
* Those methods have precedence over respective methods defined by
* superinterfaces! A corresponding check needs to be done before calling
* this method.
*/
def findMaximallySpecificSuperinterfaceMethods(
superinterfaceType: ObjectType,
name: String,
descriptor: MethodDescriptor,
analyzedSuperinterfaceTypes: UIDSet[ObjectType] = UIDSet.empty
): ( /*analyzed types*/ UIDSet[ObjectType], Set[Method]) = {
ProjectLike.findMaximallySpecificSuperinterfaceMethods(
superinterfaceType, name, descriptor, analyzedSuperinterfaceTypes
)(this.classFile, this.classHierarchy, this.logContext)
}
/**
* Computes the maximally specific superinterface method with the given name
* and descriptor
*
* @param superinterfaceTypes A set of interfaces which potentially declare a method
* with the given name and descriptor.
*/
def findMaximallySpecificSuperinterfaceMethods(
superinterfaceTypes: UIDSet[ObjectType],
name: String,
descriptor: MethodDescriptor,
analyzedSuperinterfaceTypes: UIDSet[ObjectType]
): ( /*analyzed types*/ UIDSet[ObjectType], Set[Method]) = {
ProjectLike.findMaximallySpecificSuperinterfaceMethods(
superinterfaceTypes, name, descriptor, analyzedSuperinterfaceTypes
)(this.classFile, this.classHierarchy, this.logContext)
}
/**
* Resolves a symbolic reference to a method defined by a class (not interface) type.
*
* @return [[org.opalj.Success]]`(method)` if the method was found;
* `Empty` if the project is incomplete and the method could not be found;
* `Failure` if the method could not be found though the project is seemingly complete.
* I.e., if `Failure` is returned the method is not defined by a concrete class
* and is either a default method defined by an interface or the analyzed code
* basis is inconsistent.
*/
def resolveClassMethodReference(
receiverType: ObjectType,
name: String,
descriptor: MethodDescriptor
): Result[Method] = {
project.classFile(receiverType) match {
case Some(classFile) =>
assert(
!classFile.isInterfaceDeclaration,
{
val methodInfo = descriptor.toJava(receiverType.toJava, name)
s"the method is defined in an interface $methodInfo"
}
)
def resolveSuperclassMethodReference(): Result[Method] = {
classFile.superclassType match {
case Some(superclassType) =>
resolveClassMethodReference(superclassType, name, descriptor)
case None =>
// the current type is java.lang.Object and the method was not found
Failure
}
}
// [FROM THE SPECIFICATION]]
// A method is signature polymorphic if all of the following conditions hold :
// - It is declared in the java.lang.invoke.MethodHandle class.
// - It has a single formal parameter of type Object[].
// - It has the ACC_VARARGS and ACC_NATIVE flags set.
val isPotentiallySignaturePolymorphicCall =
(receiverType eq ObjectType.MethodHandle) ||
(receiverType eq ObjectType.VarHandle)
if (isPotentiallySignaturePolymorphicCall) {
val methods = classFile.findMethod(name)
methods match {
case List(method) =>
if (method.isNativeAndVarargs &&
(method.descriptor == SignaturePolymorphicMethodObject ||
method.descriptor == SignaturePolymorphicMethodVoid ||
method.descriptor == SignaturePolymorphicMethodBoolean))
Success(method) // the resolved method is signature polymorphic
else if (method.descriptor == descriptor)
Success(method) // "normal" resolution of a method
else
resolveSuperclassMethodReference()
case _ =>
methods.find(m => m.descriptor == descriptor) match {
case None => resolveSuperclassMethodReference()
case Some(resolvedMethod) => Success(resolvedMethod)
}
}
} else {
classFile.findMethod(name, descriptor) match {
case None => resolveSuperclassMethodReference()
case Some(resolvedMethod) => Success(resolvedMethod)
}
}
case None => Empty
}
}
/**
* Returns true if the method defined by the given class type is a signature polymorphic
* method. (See JVM 9 Spec. for details.)
*/
//TODO add method that lookup the defining class type
def isSignaturePolymorphic(definingClassType: ObjectType, method: Method): Boolean = {
(
(definingClassType eq ObjectType.MethodHandle) ||
(definingClassType eq ObjectType.VarHandle)
) &&
method.descriptor.parametersCount == 1 &&
method.descriptor.parameterType(0) == ArrayType.ArrayOfObject &&
method.isNativeAndVarargs
}
/**
* Returns true if the descriptor is a signature polymorphic method for the given class type.
* (See JVM 9 Spec. for details.)
*/
def isSignaturePolymorphic(
definingClassType: ObjectType,
descriptor: MethodDescriptor
): Boolean = {
(definingClassType eq ObjectType.MethodHandle) &&
descriptor == SignaturePolymorphicMethodObject ||
(definingClassType eq ObjectType.VarHandle) &&
(descriptor == SignaturePolymorphicMethodObject ||
descriptor == SignaturePolymorphicMethodVoid ||
descriptor == SignaturePolymorphicMethodBoolean)
}
/**
* Returns true if the signature is a signature polymorphic method for the given class type.
* (See JVM 9 Spec. for details.)
*/
def isSignaturePolymorphic(
definingClassType: ObjectType,
name: String,
descriptor: MethodDescriptor
): Boolean = {
(definingClassType eq ObjectType.MethodHandle) &&
descriptor == SignaturePolymorphicMethodObject &&
(name == "invoke" || name == "invokeExact") ||
(definingClassType eq ObjectType.VarHandle) &&
(descriptor == SignaturePolymorphicMethodObject ||
descriptor == SignaturePolymorphicMethodVoid ||
descriptor == SignaturePolymorphicMethodBoolean)
}
/**
* Returns the method which will be called by the respective
* [[org.opalj.br.instructions.INVOKESTATIC]] instruction.
*/
def staticCall(callerClassType: ObjectType, i: INVOKESTATIC): Result[Method] = {
staticCall(callerClassType, i.declaringClass, i.isInterface, i.name, i.methodDescriptor)
}
/**
* Returns the method that will be called by the respective invokestatic call.
*
* @return [[org.opalj.Success]] `(method)` if the method was found;
* `Failure` if the project is inconsistent.
* `Empty` if the method could not be found in the available classes (i.e., the
* project is incomplete).
*/
def staticCall(
callerClassType: ObjectType,
declaringClassType: ObjectType,
isInterface: Boolean,
name: String,
descriptor: MethodDescriptor
): Result[Method] = {
// Recall that the invokestatic instruction:
// "... gives the name and descriptor of the method as well as a symbolic reference to
// the class or interface in which the method is to be found.
// However, in case of interfaces no lookup in superclasses is done!
if (isInterface) {
classFile(declaringClassType) match {
case Some(cf) => cf.findMethod(name, descriptor) match {
case Some(method) if method.isAccessibleBy(callerClassType, nests) =>
Success(method)
case _ => Empty
}
case None => Empty
}
} else {
resolveClassMethodReference(declaringClassType, name, descriptor) match {
case s @ Success(method) =>
if (method.isAccessibleBy(callerClassType, nests)) s else Empty
case e =>
e
}
}
}
def specialCall(callerClassType: ObjectType, i: INVOKESPECIAL): Result[Method] = {
specialCall(callerClassType, i.declaringClass, i.isInterface, i.name, i.methodDescriptor)
}
def nonVirtualCall(
callerClassType: ObjectType,
i: NonVirtualMethodInvocationInstruction
): Result[Method] = {
if (i.opcode == INVOKESPECIAL.opcode) {
specialCall(callerClassType, i.asINVOKESPECIAL)
} else { // i.opcode == INVOKESTATIC.opcode
staticCall(callerClassType, i.asINVOKESTATIC)
}
}
/**
* Returns the instance method/initializer which is called by an invokespecial instruction.
*
* @note Virtual method call resolution is not necessary; the call target is
* either a constructor, a method in the given class or a super method/constructor.
* However, in the first and last case it may be possible that we can't find the method
* because of an inconsistent or incomplete project.
*
* @return One of the following three values:
* - [[org.opalj.Success]] `(method)` if the method was found;
* - `Failure` if the project is inconsistent; i.e., the target class file is found,
* but the method cannot be found. `Failure` is returned on a best effort basis.
* - `Empty`.
*/
def specialCall(
callerClassType: ObjectType,
initialDeclaringClassType: ObjectType, // an interface or class type to be precise
isInterface: Boolean,
name: String, // an interface or class type to be precise
descriptor: MethodDescriptor
): Result[Method] = {
/* FROM THE SPEC
If all of the following are true, let C be the direct superclass of the current class:
• The resolved method is not an instance initialization method (§2.9.1).
• If the symbolic reference names a class (not an interface), then that class is a
superclass of the current class.
• The ACC_SUPER flag is set for the class file (§4.1).
Otherwise, let C be the class or interface named by the symbolic reference.
*/
val declaringClassType =
if (name != "" &&
(callerClassType ne initialDeclaringClassType) && // <= handles private calls
classHierarchy.isInterface(initialDeclaringClassType).isNo) {
// Let's select the direct superclass (if it is available; otherwise we use the
// declared class as a fallback.)
classHierarchy.superclassType(callerClassType).getOrElse(initialDeclaringClassType)
} else {
initialDeclaringClassType
}
// ... default methods cannot override methods from java.lang.Object
// ... in case of super method calls (not initializers), we can use
// "instanceMethods" to find the method, because the method has to
// be an instance method, must not be abstract and must not be private.
// ... the receiver type of super initializer calls is always explicitly given
classFile(declaringClassType) match {
case Some(classFile) =>
if (classFile.isInterfaceDeclaration != isInterface)
Failure
else {
classFile.findMethod(name, descriptor) match {
case Some(method) =>
if (method.isAccessibleBy(callerClassType, nests))
Success(method)
else
Empty
case None if name == "" => Failure // initializer not found...
case _ =>
// We have to find the (maximally specific) super method, which is,
// unless we have an inconsistent code base, unique.
find(instanceMethods(declaringClassType)) { definedMethodContext =>
val definedMethod = definedMethodContext.method
definedMethod.compare(name, descriptor)
} match {
case Some(mdc) =>
if (mdc.method.isAccessibleBy(callerClassType, nests))
Success(mdc.method)
else
Empty
case None => Empty
}
}
}
case None => Empty
}
}
/**
* Returns the (instance) method that would be called when we have an instance of
* the given receiver type. I.e., using this method is suitable only when the runtime
* type, which is the receiver of the method call, is precisely known!
*
* == Examples ==
* {{{
* class A {def foo() = {} }
* class B extends A {/*inherits, but does not override foo()*/}
* class C extends B { def foo() = {} }
* val b = new B();
* b.foo() // <= in this case the method defined by A will be returned.
* val c = new C();
* c.foo() // <= in this case the method defined by C will be returned.
* }}}
*
* This method supports default methods and signature polymorphic calls; i.e., the
* descriptor of the retuned methods may not be equal to the given method descriptor.
*
* @param callerClassType The object type which defines the method which performs the call.
* This information is required if the call target has (potentially) default
* visibility. (Note that this - in general - does not replace the need to perform an
* accessibility check.)
* @param receiverType A class type or an array type; never an interface type.
*/
def instanceCall(
callerClassType: ObjectType,
receiverType: ReferenceType,
name: String,
descriptor: MethodDescriptor
): Result[Method] = {
if (receiverType.isArrayType) {
return Result(ObjectClassFile flatMap { cf => cf.findMethod(name, descriptor) });
}
val receiverClassType = receiverType.asObjectType
val mdcResult = lookupVirtualMethod(callerClassType, receiverClassType, name, descriptor)
mdcResult flatMap { mdc =>
if (!mdc.method.isPrivate || mdc.method.isAccessibleBy(callerClassType, nests))
Success(mdc.method)
else
Empty
}
}
def interfaceCall(callerType: ObjectType, i: INVOKEINTERFACE): Set[Method] = {
interfaceCall(callerType, i.declaringClass, i.name, i.methodDescriptor)
}
private val useJava11CallSemantics: Boolean = {
val key = ProjectLike.EnforceJava11CallSemanticsConfigKey
val forceJ11semantics: Boolean =
try {
config.getBoolean(key)
} catch {
case t: Throwable =>
error("project configuration", s"couldn't read: $key", t)
false
}
val (useJ11semantics, reason) = if (forceJ11semantics) {
(true, "(enforced by config)")
} else {
val requiredVersion = requiredJVMVersion
(requiredVersion >= Java11MajorVersion, s"(required JVM version is $requiredVersion)")
}
info(
"project configuration",
s"${if (useJ11semantics) "" else "not "}using Java 11+ call semantics "+reason
)
useJ11semantics
}
/**
* Returns the methods that may be called by an [[org.opalj.br.instructions.INVOKEINTERFACE]]
* call if the precise runtime type is not known. (If the precise runtime type is known, use
* `instanceCall` to get the target method.)
*
* @note '''Caching the result (in particular when the call graph is computed)
* is recommended as the computation is expensive.''' In other words, this
* function is meant to be used as a foundation for call graph construction
* algorithms.
*
* @note Keep in mind that the following is legal (byte)code:
* {{{
* class X { void m(){ System.out.println("X.m"); } }
* interface I { void m(); }
* class Z extends X implements I {}
* }}}
* Hence, we also have to consider inherited methods and just considering the
* methods defined by subclasses is not sufficient! In other words, the result
* can contain methods (here, `X.m`) defined by classes which are not subtypes
* of the given interface type!
*
* @return The set of potentially called methods. The set will be empty if the target class
* is not defined as part of the analyzed code base.
*/
def interfaceCall(
callerType: ObjectType,
declaringClass: ObjectType, // an interface or class type to be precise
name: String,
descriptor: MethodDescriptor
): Set[Method] = {
var methods = Set.empty[Method]
// (1) consider the method defined by the super type or this type...
// Depending on the set of open/closed packages it may be the case that the method
// cannot be a receiver, because it is actually always overridden; however, we don't
// do any checks related to this issue.
val initialMethodsOption = instanceMethods.get(declaringClass)
if (initialMethodsOption.isEmpty)
return methods;
find(initialMethodsOption.get) { mdc =>
mdc.method.compare(name, descriptor)
} foreach (mdc => methods += mdc.method)
if (methods.nonEmpty) {
val method = methods.head
if (!method.isPublic) {
if (method.isPrivate && useJava11CallSemantics) {
// The method is private, thus it is selected (JVM 11 Spec Section 5.4.6)
// However, access control may still fail
if (!method.isAccessibleBy(callerType, nests))
return Set.empty[Method];
return methods;
} else {
methods = Set.empty[Method]
}
}
}
// (2) methods of strict subtypes (always necessary, because we have an interface)
classHierarchy.foreachSubtypeCF(declaringClass, reflexive = false) { subtypeCF =>
val subtype = subtypeCF.thisType
val mdc = find(instanceMethods(subtype)) { mdc => mdc.method.compare(name, descriptor) }
mdc match {
case Some(mdc) =>
if (mdc.isPublic)
methods += mdc.method
// This is an overapproximation, if the inherited concrete method is
// always overridden by all concrete subtypes and subtypeCF
// is an abstract class in a closed package/module
if (!mdc.method.isPrivate)
methods ++=
overriddenBy(mdc.method).iterator.filter { m =>
m.classFile.thisType isSubtypeOf subtype
}
// for interfaces we have to continue, because we may have inherited a
// a concrete method from a class type which is not in the set of
// overriddenBy methods
subtypeCF.isInterfaceDeclaration
case _ /*None*/ =>
true
}
}
methods
}
/**
* Convenience method; see `virtualCall(callerPackageName:String,declaringType:ReferenceType*`
* for details.
*/
def virtualCall(callerType: ObjectType, i: INVOKEVIRTUAL): SomeSet[Method] = {
virtualCall(callerType, i.declaringClass, i.name, i.methodDescriptor)
}
/**
* Returns the set of methods that may be called by an invokevirtual call, if
* the receiver type is unknown or effectively encompasses all subtypes it
* is recommended to use [[instanceCall]].
*
* @note As in case of instance call, the returned method may have a different
* descriptor if we have a signature polymorphic call!
*/
def virtualCall(
callerType: ObjectType,
declaringType: ReferenceType, // an interface, class or array type to be precise
name: String,
descriptor: MethodDescriptor
): SomeSet[Method] = {
if (declaringType.isArrayType) {
return instanceCall(ObjectType.Object, ObjectType.Object, name, descriptor).toSet
}
// In the following we opted for implementing some support for the
// different possibilities that exist w.r.t. where the defined method
// is found. This is done to speed up the computation
// of the set of methods (vs. using a very generic approach)!
val declaringClassType = declaringType.asObjectType
var methods = mutable.Set.empty[Method]
val initialMethodsOption = instanceMethods.get(declaringClassType)
if (initialMethodsOption.isEmpty)
return methods;
val callerPackageName = callerType.packageName
// Let's find the (concrete) method defined by this type or a supertype if it exists.
// We have to check the declaring package if the method has package visibility to ensure
// that we find the correct method!
find(initialMethodsOption.get) { mdc =>
mdc.compareAccessibilityAware(callerPackageName, name, descriptor)
} foreach (mdc => methods += mdc.method)
if (methods.nonEmpty) {
val method = methods.head
if (method.isPrivate) {
// The concrete method is private, thus it is selected (JVM 11 Spec Section 5.4.6)
// However, access control may still fail
if (!method.isAccessibleBy(callerType, nests))
return SomeSet.empty[Method];
return methods;
} else if (method.classFile.thisType eq declaringClassType) {
// The (concrete) method belongs to this class... hence, we just need to
// get all methods which override (reflexive) this method and are done.
return overriddenBy(method);
} else {
// ... we cannot use the overriddenBy methods because this could return
// methods belonging to classes which are not a subtype of the given
// declaring class.
classHierarchy.foreachSubtypeCF(declaringClassType, reflexive = false) { subtypeCF =>
val subtype = subtypeCF.thisType
val mdcOption = find(instanceMethods(subtype)) { mdc =>
mdc.compareAccessibilityAware(callerPackageName, name, descriptor)
}
if (mdcOption.nonEmpty && (mdcOption.get.method ne method)
&& !mdcOption.get.method.isPrivate) {
methods ++= overriddenBy(mdcOption.get.method)
false // we don't have to look into furthersubtypes
} else {
true
}
}
return methods;
}
}
// We just have to collect the methods in the subtypes... unless we have
// a call to a signature polymorphic method!
def findSignaturePolymorphicMethod(
descriptor: MethodDescriptor
): Option[MethodDeclarationContext] = {
find(instanceMethods(declaringClassType)) { mdc =>
mdc.compareAccessibilityAware(callerPackageName, name, descriptor)
}
}
if (MethodHandleClassFile.isDefined && MethodHandleSubtypes.contains(declaringClassType)) {
val mdcOption = findSignaturePolymorphicMethod(SignaturePolymorphicMethodObject)
if (mdcOption.isDefined) {
val method = mdcOption.get.method
if (method.isNativeAndVarargs && (method.classFile eq MethodHandleClassFile.get)) {
return Set(method);
}
}
}
if (VarHandleClassFile.isDefined && VarHandleSubtypes.contains(declaringClassType)) {
val mdcOption =
findSignaturePolymorphicMethod(SignaturePolymorphicMethodObject).orElse(
findSignaturePolymorphicMethod(SignaturePolymorphicMethodVoid).orElse(
findSignaturePolymorphicMethod(SignaturePolymorphicMethodBoolean)
)
)
if (mdcOption.isDefined) {
val method = mdcOption.get.method
if (method.isNativeAndVarargs && (method.classFile eq VarHandleClassFile.get)) {
return Set(method);
}
}
}
classHierarchy.foreachSubtypeCF(declaringClassType, reflexive = false) { subtypeCF =>
val subtype = subtypeCF.thisType
val mdcOption = find(instanceMethods(subtype)) { mdc =>
mdc.compareAccessibilityAware(callerPackageName, name, descriptor)
}
mdcOption match {
case Some(mdc) if !mdc.method.isPrivate =>
if (methods.isEmpty) {
methods = mutable.Set.from(overriddenBy(mdc.method))
} else {
methods ++= overriddenBy(mdc.method)
}
false // we don't have to look into furthersubtypes
case _ /*None*/ =>
true
}
}
methods
}
}
object ProjectLike {
/**
* Computes the set of maximally specific superinterface methods with the
* given name and descriptor.
*
* @note This method requires that the class hierarchy is already computed.
* It does not require `instanceMethods`.
* @note '''This method does not consider methods defined by `java.lang.Object`'''!
* Those methods have precedence over respective methods defined by
* superinterfaces! A corresponding check needs to be done before calling
* this method.
*/
def findMaximallySpecificSuperinterfaceMethods(
superinterfaceType: ObjectType,
name: String,
descriptor: MethodDescriptor,
analyzedSuperinterfaceTypes: UIDSet[ObjectType] = UIDSet.empty
)(
implicit
objectTypeToClassFile: ObjectType => Option[ClassFile],
classHierarchy: ClassHierarchy,
logContext: LogContext
): ( /*analyzed types*/ UIDSet[ObjectType], Set[Method]) = {
val newAnalyzedSuperinterfaceTypes = analyzedSuperinterfaceTypes + superinterfaceType
// the superinterfaceTypes in which it is potentially relevant to search for methods
val superinterfaceTypes: UIDSet[ObjectType] =
classHierarchy.superinterfaceTypes(superinterfaceType).getOrElse(UIDSet.empty) --
analyzedSuperinterfaceTypes
objectTypeToClassFile(superinterfaceType) match {
case Some(classFile) =>
if (!classFile.isInterfaceDeclaration) {
OPALLogger.warn(
"project configuration",
"finding the maximally specific superinterface methods failed: "+
s"${superinterfaceType.toJava} is not an interface and ignored"
)
(analyzedSuperinterfaceTypes ++ superinterfaceTypes + superinterfaceType, Set.empty)
} else {
classFile.findMethod(name, descriptor) match {
case Some(method) if !method.isPrivate && !method.isStatic =>
val analyzedTypes = newAnalyzedSuperinterfaceTypes ++ superinterfaceTypes
(analyzedTypes, Set(method))
case _ /* None OR "the method was either private or static" */ =>
if (superinterfaceTypes.isEmpty) {
(newAnalyzedSuperinterfaceTypes, Set.empty)
} else if (superinterfaceTypes.isSingletonSet) {
findMaximallySpecificSuperinterfaceMethods(
superinterfaceTypes.head,
name, descriptor,
newAnalyzedSuperinterfaceTypes
)
} else {
findMaximallySpecificSuperinterfaceMethods(
superinterfaceTypes,
name, descriptor,
newAnalyzedSuperinterfaceTypes
)
}
}
}
case None =>
(analyzedSuperinterfaceTypes ++ superinterfaceTypes + superinterfaceType, Set.empty)
}
}
/**
* Computes the maximally specific superinterface method with the given name
* and descriptor
*
* @note This method requires that the class hierarchy is already computed.
* It does not required `instanceMethods`.
* @param superinterfaceTypes A set of interfaces which potentially declare a method
* with the given name and descriptor.
*/
def findMaximallySpecificSuperinterfaceMethods(
superinterfaceTypes: UIDSet[ObjectType],
name: String,
descriptor: MethodDescriptor,
analyzedSuperinterfaceTypes: UIDSet[ObjectType]
)(
implicit
objectTypeToClassFile: (ObjectType) => Option[ClassFile],
classHierarchy: ClassHierarchy,
logContext: LogContext
): ( /*analyzed types*/ UIDSet[ObjectType], Set[Method]) = {
val anchor = ((analyzedSuperinterfaceTypes, Set.empty[Method]))
superinterfaceTypes.foldLeft(anchor) { (currentResult, interfaceType) =>
val (currentAnalyzedSuperinterfaceTypes, currentMethods) = currentResult
val (analyzedSuperinterfaceTypes, methods) =
findMaximallySpecificSuperinterfaceMethods(
interfaceType, name, descriptor,
currentAnalyzedSuperinterfaceTypes
)
val allMethods = currentMethods ++ methods
if (allMethods.isEmpty || allMethods.size == 1) {
(analyzedSuperinterfaceTypes, allMethods /*empty or singleton set*/ )
} else {
// When we reach this point, we may have a situation such as:
// intf A { default void foo(){} }
// intf B extends A { default void foo(){} }
// intf C extends A { }
// intf D extends B { }
// and we started the analysis with the set {C,D} and
// first selected C (hence, first found A.foo).
//
// We now have to determine the maximally specific method.
// Both, the set of `currentMethods` and also the set of `methods`
// each only contains maximally specific methods w.r.t. their
// set.
var currentMaximallySpecificMethods = currentMethods
var additionalMaximallySpecificMethods = Set.empty[Method]
methods foreach { method =>
if (!currentMethods.contains(method)) {
val newMethodDeclaringClassType = method.classFile.thisType
var addNewMethod = true
currentMaximallySpecificMethods =
currentMaximallySpecificMethods.filter { currentMaximallySpecificMethod =>
val specificMethodDeclaringClassType = currentMaximallySpecificMethod.classFile.thisType
if (specificMethodDeclaringClassType isSubtypeOf newMethodDeclaringClassType) {
addNewMethod = false
true
} else if (newMethodDeclaringClassType isSubtypeOf specificMethodDeclaringClassType) {
false
} else {
//... we have an incomplete class hierarchy;
// let's keep both methods
true
}
}
if (addNewMethod) additionalMaximallySpecificMethods += method
}
}
currentMaximallySpecificMethods ++= additionalMaximallySpecificMethods
val concreteMaximallySpecificMethods = currentMaximallySpecificMethods.filter(!_.isAbstract)
if (concreteMaximallySpecificMethods.isEmpty) {
// We have not yet found any method or we may have multiple abstract methods...
(analyzedSuperinterfaceTypes, currentMaximallySpecificMethods)
} else {
(analyzedSuperinterfaceTypes, concreteMaximallySpecificMethods)
}
}
}
}
private val EnforceJava11CallSemanticsConfigKey =
"org.opalj.br.Project.enforceJava11CallSemantics"
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy