Skip to content

Commit

Permalink
Handle method references (#352)
Browse files Browse the repository at this point in the history
Method references should be solvable, but only if their parameter types
match the type arguments in its corresponding functional interface (for
example, `Function<?, ?>` doesn't match `String foo(int x)` but
`Function<Integer, ?>` does). The test case (a bit of a large one) is in
`SyntheticMethodRefsTest`. Thanks!
  • Loading branch information
theron-wang authored Aug 9, 2024
1 parent 0201a94 commit 47fb15f
Show file tree
Hide file tree
Showing 27 changed files with 680 additions and 38 deletions.
2 changes: 1 addition & 1 deletion CI_Latest_run_percentage.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
88.61
89.01
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ task checkExpectedOutputCompilesFor(type: Exec) {
if (project.hasProperty("testName")) {
workingDir("src/test/resources")
if (System.getProperty('os.name').toLowerCase().startsWith('windows')) {
// TODO: fix this
print "checkExpectedOutputCompilesFor is not supported on Windows"
// Avoid '..' is not a command error
def scriptPath = file("typecheck_one_test.bat").absolutePath
commandLine "cmd", "/c", scriptPath, project.property("testName")
} else {
commandLine "sh", "../../../typecheck_one_test.sh", project.property("testName")
}
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/org/checkerframework/specimin/JavaLangUtils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.checkerframework.specimin;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/** Utility class for questions related to the java.lang package. */
Expand Down Expand Up @@ -49,6 +51,9 @@ private JavaLangUtils() {
*/
private static final Set<String> knownFinalJdkTypes = new HashSet<>();

/** A map of primitive names (int, short, etc.) to their object representations */
private static final Map<String, String> primitivesToObjects = new HashMap<>();

static {
primitives.add("int");
primitives.add("short");
Expand All @@ -59,6 +64,15 @@ private JavaLangUtils() {
primitives.add("double");
primitives.add("char");

primitivesToObjects.put("int", "Integer");
primitivesToObjects.put("short", "Short");
primitivesToObjects.put("byte", "Byte");
primitivesToObjects.put("long", "Long");
primitivesToObjects.put("boolean", "Boolean");
primitivesToObjects.put("float", "Float");
primitivesToObjects.put("double", "Double");
primitivesToObjects.put("char", "Character");

javaLangClassesAndInterfaces.add("AbstractMethodError");
javaLangClassesAndInterfaces.add("Appendable");
javaLangClassesAndInterfaces.add("ArithmeticException");
Expand Down Expand Up @@ -301,6 +315,33 @@ public static String[] getTypesForOp(String binOp) {
}
}

/**
* Is a type primitive (int, char, boolean, etc.)? This method returns false for boxed types
* (Integer, Character, Boolean, etc.)
*
* @param type the type to check
* @return true iff the type is primitive
*/
public static boolean isPrimitive(String type) {
return primitives.contains(type);
}

/**
* Converts a primitive to its boxed type (i.e. int --> Integer)
*
* @param primitive the primitive type (int, boolean, char, etc.)
* @return the boxed type (int --> Integer, char --> Character)
*/
public static String getPrimitiveAsBoxedType(String primitive) {
String converted = primitivesToObjects.get(primitive);

if (converted == null) {
throw new IllegalArgumentException(primitive + " is not a primitive type");
}

return converted;
}

/**
* Returns true iff both input types are java.lang.Class followed by some type parameter.
*
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/org/checkerframework/specimin/JavaParserUtil.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.checkerframework.specimin;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.AnnotationDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
Expand All @@ -12,10 +14,13 @@
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.nodeTypes.NodeWithDeclaration;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.resolution.UnsolvedSymbolException;
import com.github.javaparser.resolution.types.ResolvedReferenceType;
import com.github.javaparser.resolution.types.ResolvedType;
import com.google.common.base.Splitter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -200,6 +205,46 @@ public static Node getEnclosingClassLike(Node node) {
return parent;
}

/**
* Given a String of types (separated by commas), return a List of these types, with any
* primitives converted to their object counterparts. Use this instead of {@code .split(", ")} to
* properly handle generics.
*
* @param commaSeparatedTypes A string of comma separated types
* @return a list of strings representing the types in commaSeparatedTypes
*/
public static List<String> getReferenceTypesFromCommaSeparatedString(String commaSeparatedTypes) {
if (commaSeparatedTypes == null || commaSeparatedTypes.isBlank()) {
return Collections.EMPTY_LIST;
}

// Splitting them is simply to change primitives to objects so we do not
// get an error when parsing in StaticJavaParser (note that this array)
// may contain incomplete types like ["Map<String", "Object>"]
String[] tokens = commaSeparatedTypes.split(",");

for (int i = 0; i < tokens.length; i++) {
if (JavaLangUtils.isPrimitive(tokens[i].trim())) {
tokens[i] = JavaLangUtils.getPrimitiveAsBoxedType(tokens[i].trim());
}
}

// Parse as a generic type, then get the type arguments
// This way we can properly differentiate between commas within type arguments
// versus actual commas in javac error messages
Type parsed = StaticJavaParser.parseType("ToParse<" + String.join(", ", tokens) + ">");

List<String> types = new ArrayList<>();
NodeList<Type> typeArguments = parsed.asClassOrInterfaceType().getTypeArguments().orElse(null);

if (typeArguments != null) {
for (Type typeArgument : typeArguments) {
types.add(typeArgument.toString());
}
}
return types;
}

/**
* Returns true iff the innermost enclosing class/interface is an enum.
*
Expand Down
98 changes: 94 additions & 4 deletions src/main/java/org/checkerframework/specimin/JavaTypeCorrect.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class JavaTypeCorrect {
* This map is for type correcting. The key is the name of the current incorrect type, and the
* value is the name of the desired correct type.
*/
private Map<String, String> typeToChange;
private final Map<String, String> typeToChange;

/**
* A map that associates the file directory with the set of fully qualified names of types used
Expand All @@ -53,13 +53,22 @@ class JavaTypeCorrect {
* because the worst thing that might happen is that an extra synthetic class might accidentally
* extend or implement an unnecessary interface.
*/
private Map<String, String> extendedTypes = new HashMap<>();
private final Map<String, String> extendedTypes = new HashMap<>();

/**
* This map associates the name of a class with the name of the unresolved interface due to
* missing method implementations.
*/
private Map<String, String> classAndUnresolvedInterface = new HashMap<>();
private final Map<String, String> classAndUnresolvedInterface = new HashMap<>();

/**
* This map associates a method reference usage to a map of argument corrections, containing the
* method reference and the correct number of arguments
*/
private final Map<String, String> methodRefToCorrectParameters = new HashMap<>();

/** This map associates a method reference usage to whether its return type is void or not. */
private final Map<String, Boolean> methodRefVoidness = new HashMap<>();

/** The name used for a synthetic, unconstrained type variable. */
public static final String SYNTHETIC_UNCONSTRAINED_TYPE = "SyntheticUnconstrainedType";
Expand Down Expand Up @@ -99,6 +108,24 @@ public Map<String, String> getClassAndUnresolvedInterface() {
return classAndUnresolvedInterface;
}

/**
* Get the value of methodRefToCorrectParameters.
*
* @return the value of methodRefToCorrectParameters.
*/
public Map<String, String> getMethodRefToCorrectParameters() {
return methodRefToCorrectParameters;
}

/**
* Get the value of methodRefVoidness.
*
* @return the value of methodRefVoidness.
*/
public Map<String, Boolean> getMethodRefVoidness() {
return methodRefVoidness;
}

/**
* Get the simple names of synthetic classes that should extend or implement a class/interface (it
* is not known at this point which). Both keys and values are simple names, due to javac
Expand Down Expand Up @@ -178,6 +205,7 @@ public void runJavacAndUpdateTypes(String filePath) {
// * incompatible equality constraints
// * bad operand types for binary operators
// * for-each not applicable to expression type
// * incompatiable method reference types

// These are temporaries for the equality constraints case.
String[] firstConstraints = {"equality constraints: "};
Expand All @@ -192,6 +220,10 @@ public void runJavacAndUpdateTypes(String filePath) {
String loopType = null;
boolean lookingForLoopType = false;

// These temporaries are for the invalid method reference cases.
boolean lookingForInvalidMethodReference = false;
String methodReferenceUsage = null;

StringBuilder lines = new StringBuilder("\n");

lines:
Expand Down Expand Up @@ -248,9 +280,67 @@ public void runJavacAndUpdateTypes(String filePath) {
continue;
}

if (lookingForInvalidMethodReference) {
if (line.contains("::")) {
methodReferenceUsage = line;
} else if (line.contains("^")) {
if (methodReferenceUsage == null) {
throw new RuntimeException("Method reference not found");
}

// This is the start of the method reference; travel forwards until we hit a non-
// alphanumeric character, except for :
int start = line.indexOf("^");

int end = start;
while (end < methodReferenceUsage.length()
&& (Character.isLetterOrDigit(methodReferenceUsage.charAt(end))
|| methodReferenceUsage.charAt(end) == ':')) {
end++;
}

methodReferenceUsage = methodReferenceUsage.substring(start, end);
}
// method x in class y cannot be applied to given types
// then, it gives you a line with required: and all the necessary parameters
else if (line.contains("required:")) {
if (methodReferenceUsage == null) {
throw new RuntimeException("Method reference not found");
}
if (line.contains("no arguments")) {
methodRefToCorrectParameters.put(methodReferenceUsage, "");
} else {
methodRefToCorrectParameters.put(
methodReferenceUsage, line.trim().substring("required:".length()).trim());
}

lookingForInvalidMethodReference = false;
methodReferenceUsage = null;
continue;
}
// handle method return type (this is mutually exclusive with
// argument types; if argument types are not valid, this error message
// will not show up)
else if (line.contains("void cannot be converted to")) {
if (methodReferenceUsage == null) {
throw new RuntimeException("Method reference not found");
}
methodRefVoidness.put(methodReferenceUsage, true);
lookingForInvalidMethodReference = false;
methodReferenceUsage = null;
continue;
}
}

if (line.contains("error: incompatible types")
|| line.contains("error: incomparable types")) {
updateTypeToChange(line, filePath);
if (line.contains("invalid method reference")
|| line.contains("bad return type in method reference")) {
lookingForInvalidMethodReference = true;
continue;
} else {
updateTypeToChange(line, filePath);
}
continue lines;
}
if (line.contains("is not compatible with")) {
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/org/checkerframework/specimin/SpeciminRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ private static void performMinimizationImpl(

Map<String, String> typesToChange = new HashMap<>();
Map<String, String> classAndUnresolvedInterface = new HashMap<>();
Map<String, String> methodRefToCorrectParameters = new HashMap<>();
Map<String, Boolean> methodRefToVoidness = new HashMap<>();

// This is a defense against infinite loop bugs. The idea is this:
// if we encounter the same set of outputs three times, that's a good indication
Expand Down Expand Up @@ -363,10 +365,20 @@ private static void performMinimizationImpl(
typeCorrecter.correctTypesForAllFiles();
typesToChange = typeCorrecter.getTypeToChange();
classAndUnresolvedInterface = typeCorrecter.getClassAndUnresolvedInterface();
methodRefToCorrectParameters = typeCorrecter.getMethodRefToCorrectParameters();
methodRefToVoidness = typeCorrecter.getMethodRefVoidness();
boolean changeAtLeastOneType = addMissingClass.updateTypes(typesToChange);
boolean extendAtLeastOneType =
addMissingClass.updateTypesWithExtends(typeCorrecter.getExtendedTypes());
boolean atLeastOneTypeIsUpdated = changeAtLeastOneType || extendAtLeastOneType;
boolean changeAtLeastOneMethodRef =
addMissingClass.updateMethodReferenceParameters(methodRefToCorrectParameters);
boolean changeAtLeastOneMethodReturn =
addMissingClass.updateMethodReferenceVoidness(methodRefToVoidness);
boolean atLeastOneTypeIsUpdated =
changeAtLeastOneType
|| extendAtLeastOneType
|| changeAtLeastOneMethodRef
|| changeAtLeastOneMethodReturn;

// this is case 2. We will stop addMissingClass. In the next phase,
// TargetMethodFinderVisitor will give us a meaningful exception message regarding which
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public abstract class SpeciminStateVisitor extends ModifierVisitor<Void> {
* class.fully.qualified.Name#fieldName
* @param usedMembers set containing the signatures of used members
* @param usedTypeElements set containing the signatures of used classes, enums, annotations, etc.
* @param model the modularity model
* @param existingClassesToFilePath map from existing classes to file paths
*/
public SpeciminStateVisitor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,11 +531,11 @@ public Visitable visit(MethodCallExpr call, Void p) {
return super.visit(call, p);
}
preserveMethodDecl(decl);
// Special case for lambdas to preserve artificial functional
// Special case for lambdas/method references to preserve artificial functional
// interfaces.
for (int i = 0; i < call.getArguments().size(); ++i) {
Expression arg = call.getArgument(i);
if (arg.isLambdaExpr()) {
if (arg.isLambdaExpr() || arg.isMethodReferenceExpr()) {
updateUsedClassBasedOnType(decl.getParam(i).getType());
// We should mark the abstract method for preservation as well
if (decl.getParam(i).getType().isReferenceType()) {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/checkerframework/specimin/UnsolvedMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ public boolean replaceParamWithObject(String incorrectTypeName) {
return result;
}

/**
* Corrects the parameter's type at index {@code parameter}
*
* @param parameter The parameter (index) to replace
* @param correctName The type name to replace the parameter type as
*/
public void correctParameterType(int parameter, String correctName) {
parameterList.set(parameter, correctName);
}

/** Set isStatic to true */
public void setStatic() {
isStatic = true;
Expand Down
Loading

0 comments on commit 47fb15f

Please sign in to comment.