diff --git a/sootup.core/src/main/java/sootup/core/typehierarchy/TypeHierarchy.java b/sootup.core/src/main/java/sootup/core/typehierarchy/TypeHierarchy.java index 096193ceb28..8d0a7d52b91 100644 --- a/sootup.core/src/main/java/sootup/core/typehierarchy/TypeHierarchy.java +++ b/sootup.core/src/main/java/sootup/core/typehierarchy/TypeHierarchy.java @@ -162,10 +162,10 @@ default boolean isSubtype(@Nonnull Type supertype, @Nonnull Type potentialSubtyp || supertypeName.equals("java.io.Serializable") || supertypeName.equals("java.lang.Cloneable"); } else { - throw new AssertionError("potentialSubtype has unexpected type"); + throw new IllegalStateException("potentialSubtype has unexpected type"); } } else { - throw new AssertionError("supertype has unexpected type"); + throw new IllegalStateException("supertype has unexpected type"); } } @@ -200,6 +200,6 @@ default List superClassesOf(@Nonnull ClassType classType) { Set directlyExtendedInterfacesOf(@Nonnull ClassType type); // checks if a Type is contained int the TypeHierarchy - should return the equivalent to - // View.getClass(...).isPresent() + // View.getClass(...).isPresent() - the opposite is not true! boolean contains(ClassType type); } diff --git a/sootup.core/src/main/java/sootup/core/types/PrimitiveType.java b/sootup.core/src/main/java/sootup/core/types/PrimitiveType.java index 326daeb1bae..153082e3471 100644 --- a/sootup.core/src/main/java/sootup/core/types/PrimitiveType.java +++ b/sootup.core/src/main/java/sootup/core/types/PrimitiveType.java @@ -22,6 +22,10 @@ * #L% */ +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.Map; +import java.util.Set; import javax.annotation.Nonnull; import sootup.core.jimple.visitor.TypeVisitor; @@ -50,12 +54,54 @@ public String getName() { return name; } + @Nonnull + public abstract String getBoxedName(); + @Override @Nonnull public String toString() { return name; } + private static final Map, Set>> + implicitConversionMap = + ImmutableMap + ., Set>>builder() + .put( + ByteType.class, + ImmutableSet.of( + ShortType.class, + IntType.class, + LongType.class, + FloatType.class, + DoubleType.class)) + .put( + ShortType.class, + ImmutableSet.of(IntType.class, LongType.class, FloatType.class, DoubleType.class)) + .put( + CharType.class, + ImmutableSet.of(IntType.class, LongType.class, FloatType.class, DoubleType.class)) + .put( + IntType.class, ImmutableSet.of(LongType.class, FloatType.class, DoubleType.class)) + .put(LongType.class, ImmutableSet.of(FloatType.class, DoubleType.class)) + .put(FloatType.class, ImmutableSet.of(DoubleType.class)) + .build(); + + /** + * @param fromType e.g. the method argument + * @param toType e.g. the method parameter + * @return true if type conversion is possible + */ + public static boolean isImplicitlyConvertibleTo( + @Nonnull PrimitiveType fromType, @Nonnull PrimitiveType toType) { + Class fromTypeClass = fromType.getClass(); + Class toTypeClass = toType.getClass(); + return implicitConversionMap.containsKey(fromTypeClass) + && implicitConversionMap.get(fromTypeClass).contains(toTypeClass) + || IntType.class.isAssignableFrom(fromTypeClass) + && implicitConversionMap.get(IntType.class).contains(toTypeClass); + } + @Nonnull public static ByteType getByte() { return ByteType.getInstance(); @@ -107,6 +153,12 @@ public static ByteType getInstance() { return INSTANCE; } + @Nonnull + @Override + public String getBoxedName() { + return "Byte"; + } + @Override public void accept(@Nonnull TypeVisitor v) { v.caseByteType(); @@ -124,6 +176,12 @@ public static ShortType getInstance() { return INSTANCE; } + @Nonnull + @Override + public String getBoxedName() { + return "Short"; + } + @Override public void accept(@Nonnull TypeVisitor v) { v.caseShortType(); @@ -145,6 +203,12 @@ public static IntType getInstance() { return INSTANCE; } + @Nonnull + @Override + public String getBoxedName() { + return "Integer"; + } + @Override public void accept(@Nonnull TypeVisitor v) { v.caseIntType(); @@ -162,6 +226,12 @@ public static DoubleType getInstance() { return INSTANCE; } + @Nonnull + @Override + public String getBoxedName() { + return "Double"; + } + @Override public void accept(@Nonnull TypeVisitor v) { v.caseDoubleType(); @@ -179,6 +249,12 @@ public static LongType getInstance() { return INSTANCE; } + @Nonnull + @Override + public String getBoxedName() { + return "Long"; + } + @Override public void accept(@Nonnull TypeVisitor v) { v.caseLongType(); @@ -196,6 +272,12 @@ public static FloatType getInstance() { return INSTANCE; } + @Nonnull + @Override + public String getBoxedName() { + return "Float"; + } + @Override public void accept(@Nonnull TypeVisitor v) { v.caseFloatType(); @@ -213,6 +295,12 @@ public static CharType getInstance() { return INSTANCE; } + @Nonnull + @Override + public String getBoxedName() { + return "Character"; + } + @Override public void accept(@Nonnull TypeVisitor v) { v.caseCharType(); @@ -234,6 +322,12 @@ public static BooleanType getInstance() { return INSTANCE; } + @Nonnull + @Override + public String getBoxedName() { + return "Boolean"; + } + @Override public void accept(@Nonnull TypeVisitor v) { v.caseBooleanType(); diff --git a/sootup.core/src/main/java/sootup/core/validation/InvokeArgumentValidator.java b/sootup.core/src/main/java/sootup/core/validation/InvokeArgumentValidator.java index 5d0eb766c54..8ef17aea6b0 100644 --- a/sootup.core/src/main/java/sootup/core/validation/InvokeArgumentValidator.java +++ b/sootup.core/src/main/java/sootup/core/validation/InvokeArgumentValidator.java @@ -22,8 +22,17 @@ * #L% */ +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import sootup.core.IdentifierFactory; +import sootup.core.jimple.basic.Immediate; +import sootup.core.jimple.common.expr.AbstractInvokeExpr; +import sootup.core.jimple.common.stmt.Stmt; import sootup.core.model.Body; +import sootup.core.signatures.MethodSignature; +import sootup.core.typehierarchy.TypeHierarchy; +import sootup.core.types.*; import sootup.core.views.View; /** @@ -33,15 +42,99 @@ * @author Steven Arzt */ public class InvokeArgumentValidator implements BodyValidator { - @Override public List validate(Body body, View view) { - // TODO: check copied code from old soot - /* - * for (Unit u : body.getUnits()) { Stmt s = (Stmt) u; if (s.containsInvokeExpr()) { InvokeExpr iinvExpr = - * s.getInvokeExpr(); SootMethod callee = iinvExpr.getMethod(); if (callee != null && iinvExpr.getArgCount() != - * callee.getParameterCount()) { exceptions.add(new ValidationException(s, "Invalid number of arguments")); } } } - */ - return null; + List validationException = new ArrayList<>(); + + TypeHierarchy typeHierarchy = view.getTypeHierarchy(); + IdentifierFactory identifierFactory = view.getIdentifierFactory(); + + for (Stmt stmt : body.getStmts()) { + if (!stmt.containsInvokeExpr()) { + continue; + } + + AbstractInvokeExpr invExpr = stmt.getInvokeExpr(); + MethodSignature callee = invExpr.getMethodSignature(); + List args = invExpr.getArgs(); + List parameterTypes = callee.getParameterTypes(); + + if (invExpr.getArgCount() != parameterTypes.size()) { + validationException.add( + new ValidationException( + stmt, + "Argument count '" + + invExpr.getArgCount() + + "' does not match the number of expected parameters '" + + parameterTypes.size() + + "'.")); + continue; + } + + // check argument type + ReferenceType argClassType; + Iterator iterParameters = parameterTypes.iterator(); + for (Immediate arg : args) { + Type argType = arg.getType(); + Type parameterType = iterParameters.next(); + + // handle implicit conversion cases. e.g., `int` is used as an argument of a `double` + // parameter + if (argType instanceof PrimitiveType) { + if (parameterType instanceof PrimitiveType) { + + if (argType == parameterType) { + continue; + } + + if (!PrimitiveType.isImplicitlyConvertibleTo( + (PrimitiveType) argType, (PrimitiveType) parameterType)) { + validationException.add( + new ValidationException( + stmt, + String.format( + "Invalid argument type - type conversion is not applicable to '%s' and the provided '%s'.", + parameterType, argType))); + } + continue; + } + + // prepare autoboxing test + argClassType = identifierFactory.getBoxedType(((PrimitiveType) argType)); + + } else { + argClassType = (ReferenceType) argType; + } + + // non-primitive type cases, primitive+autoboxing + if (argClassType == parameterType) { + continue; + } + + // check if the (base-) type is contained in the typehierarchy - else it throws exceptions + // TODO: incorporate into api after #874 is done + if (parameterType instanceof ClassType + && !typeHierarchy.contains((ClassType) parameterType)) { + continue; + } + + if (parameterType instanceof ArrayType) { + Type baseType = ((ArrayType) parameterType).getBaseType(); + if (baseType instanceof ClassType && !typeHierarchy.contains((ClassType) baseType)) { + continue; + } + } + + if (!typeHierarchy.isSubtype(parameterType, argClassType)) { + validationException.add( + new ValidationException( + stmt, + String.format( + "Invalid argument type. Required '%s' but provided was '%s'.", + parameterType, argType))); + } + } + } + return validationException; } } diff --git a/sootup.java.bytecode/src/main/java/sootup/java/bytecode/inputlocation/ArchiveBasedAnalysisInputLocation.java b/sootup.java.bytecode/src/main/java/sootup/java/bytecode/inputlocation/ArchiveBasedAnalysisInputLocation.java index b7e6e48f8f8..422b4ba13bb 100644 --- a/sootup.java.bytecode/src/main/java/sootup/java/bytecode/inputlocation/ArchiveBasedAnalysisInputLocation.java +++ b/sootup.java.bytecode/src/main/java/sootup/java/bytecode/inputlocation/ArchiveBasedAnalysisInputLocation.java @@ -99,8 +99,14 @@ public Optional getClassSource(@Nonnull ClassType type, @No try { FileSystem fs = fileSystemCache.get(path); final Path archiveRoot = fs.getPath("/"); - return getClassSourceInternal( - (JavaClassType) type, archiveRoot, new AsmJavaClassProvider(view)); + JavaClassType javaClassType = null; + + // FIXME: This is a temporary workaround to prevent a casting exception when passing an + // anonymous + // ClassType object e.g. from a JimpleAnalysisInputLocation + if (type instanceof JavaClassType) javaClassType = (JavaClassType) type; + else javaClassType = new JavaClassType(type.getClassName(), type.getPackageName()); + return getClassSourceInternal(javaClassType, archiveRoot, new AsmJavaClassProvider(view)); } catch (ExecutionException e) { throw new RuntimeException("Failed to retrieve file system from cache for " + path, e); } diff --git a/sootup.java.core/src/main/java/sootup/java/core/JavaIdentifierFactory.java b/sootup.java.core/src/main/java/sootup/java/core/JavaIdentifierFactory.java index 52f8a08b2fe..043d3fadab8 100644 --- a/sootup.java.core/src/main/java/sootup/java/core/JavaIdentifierFactory.java +++ b/sootup.java.core/src/main/java/sootup/java/core/JavaIdentifierFactory.java @@ -236,10 +236,7 @@ public Collection getAllPrimitiveTypes() { @Override @Nonnull public JavaClassType getBoxedType(@Nonnull PrimitiveType primitiveType) { - String name = primitiveType.getName(); - StringBuilder boxedname = new StringBuilder(name); - boxedname.setCharAt(0, Character.toUpperCase(boxedname.charAt(0))); - return getClassType(boxedname.toString(), "java.lang"); + return getClassType(primitiveType.getBoxedName(), "java.lang"); } @Override diff --git a/sootup.tests/src/test/java/sootup/tests/validator/InvokeArgumentValidatorTest.java b/sootup.tests/src/test/java/sootup/tests/validator/InvokeArgumentValidatorTest.java new file mode 100644 index 00000000000..6fef95c8a1a --- /dev/null +++ b/sootup.tests/src/test/java/sootup/tests/validator/InvokeArgumentValidatorTest.java @@ -0,0 +1,115 @@ +package sootup.tests.validator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.nio.file.Paths; +import java.util.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import sootup.core.model.SootClass; +import sootup.core.model.SourceType; +import sootup.core.validation.InvokeArgumentValidator; +import sootup.core.validation.ValidationException; +import sootup.java.bytecode.inputlocation.DefaultRTJarAnalysisInputLocation; +import sootup.jimple.parser.JimpleAnalysisInputLocation; +import sootup.jimple.parser.JimpleView; + +public class InvokeArgumentValidatorTest { + InvokeArgumentValidator invokeArgumentValidator = new InvokeArgumentValidator(); + static JimpleView jimpleView; + + @BeforeAll + public static void setup() { + + String classPath = "src/test/resources/validator/jimple"; + JimpleAnalysisInputLocation jimpleInputLocation = + new JimpleAnalysisInputLocation(Paths.get(classPath), SourceType.Application); + + // rt.jar is required since the validator uses typeHierarchy + DefaultRTJarAnalysisInputLocation defaultRTJarAnalysisInputLocation = + new DefaultRTJarAnalysisInputLocation(); + jimpleView = + new JimpleView(Arrays.asList(jimpleInputLocation, defaultRTJarAnalysisInputLocation)); + + // Speed up the class search process by limiting the search scope within application classes + final Optional classSource1 = + jimpleView.getClasses().stream() + .filter(c -> c.getType().toString().equals("jimple.InvokeArgumentValidator")) + .findFirst(); + assertFalse(classSource1.isPresent()); + } + + @Test + public void invokeArgumentValidator_success() { + List validationExceptions_success; + + validationExceptions_success = + invokeArgumentValidator.validate( + jimpleView + .getMethod( + jimpleView + .getIdentifierFactory() + .parseMethodSignature( + "")) + .get() + .getBody(), + jimpleView); + + assertEquals(0, validationExceptions_success.size()); + } + + @Test + public void testArgumentNumber_fail() { + List validationExceptions_success; + + validationExceptions_success = + invokeArgumentValidator.validate( + jimpleView + .getMethod( + jimpleView + .getIdentifierFactory() + .parseMethodSignature( + "")) + .get() + .getBody(), + jimpleView); + assertEquals(1, validationExceptions_success.size()); + } + + @Test + public void testArgumentType_fail() { + List validationExceptions_success; + + validationExceptions_success = + invokeArgumentValidator.validate( + jimpleView + .getMethod( + jimpleView + .getIdentifierFactory() + .parseMethodSignature( + "")) + .get() + .getBody(), + jimpleView); + assertEquals(2, validationExceptions_success.size()); + } + + @Test + public void testPrimitiveTypeConversion_success() { + List validationExceptions_success; + + validationExceptions_success = + invokeArgumentValidator.validate( + jimpleView + .getMethod( + jimpleView + .getIdentifierFactory() + .parseMethodSignature( + "")) + .get() + .getBody(), + jimpleView); + assertEquals(0, validationExceptions_success.size()); + } +} diff --git a/sootup.tests/src/test/resources/validator/jimple/InvokeArgumentValidator.jimple b/sootup.tests/src/test/resources/validator/jimple/InvokeArgumentValidator.jimple new file mode 100644 index 00000000000..b786bff4cc1 --- /dev/null +++ b/sootup.tests/src/test/resources/validator/jimple/InvokeArgumentValidator.jimple @@ -0,0 +1,77 @@ +public class InvokeArgumentValidator extends java.lang.Object +{ + public void () + { + InvokeArgumentValidator l0; + + l0 := @this: InvokeArgumentValidator; + specialinvoke l0.()>(); + + return; + } + + public void join(java.lang.Number,java.lang.String) + { + InvokeArgumentValidator l0; + java.lang.Number l1; + java.lang.String l2; + + + l0 := @this: InvokeArgumentValidator; + l1 := @parameter0: java.lang.Number; + l2 := @parameter1: java.lang.String; + + return; + } + + public void receivePrimitive(double) + { + InvokeArgumentValidator l0; + double l1; + + l0 := @this: InvokeArgumentValidator; + l1 := @parameter0: double; + + return; + } + + public void testPrimitiveTypeConversion_success() + { + InvokeArgumentValidator l0; + + l0 := @this: InvokeArgumentValidator; + virtualinvoke l0.(1); + + return; + } + + public void invokeArgumentValidator_success() + { + InvokeArgumentValidator l0; + + l0 := @this: InvokeArgumentValidator; + virtualinvoke l0.(1, "hello"); + + return; + } + + public void testArgumentNumber_fail() + { + InvokeArgumentValidator l0; + + l0 := @this: InvokeArgumentValidator; + virtualinvoke l0.(1); + + return; + } + + public void testArgumentType_fail() + { + InvokeArgumentValidator l0; + + l0 := @this: InvokeArgumentValidator; + virtualinvoke l0.("hello", 1); + + return; + } +} \ No newline at end of file