Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add codes for tricky parameters #46

Merged
merged 25 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
20332f8
add codes for tricky páºáºáºarameters
LoiNguyenCS Nov 16, 2023
ec4807f
codes to handle parameters
LoiNguyenCS Nov 16, 2023
348aaa5
remove debugging line
LoiNguyenCS Nov 16, 2023
bb163cc
remove < in javadoc
LoiNguyenCS Nov 16, 2023
3801117
remove unused method
LoiNguyenCS Nov 16, 2023
95968ff
re-add anonymousclass
LoiNguyenCS Nov 16, 2023
0c4b7af
codes and test cases for multi type variables
LoiNguyenCS Nov 19, 2023
7666b1b
some cleaning
LoiNguyenCS Nov 19, 2023
e601238
add throw
LoiNguyenCS Nov 19, 2023
e2188c2
some more cleaning and clarification
LoiNguyenCS Nov 19, 2023
e130de3
checkpoint: address all but one cr comment from myself
kelloggm Nov 20, 2023
3c414c2
add test for lower case class name
kelloggm Nov 20, 2023
7d6261b
fix expected test output
kelloggm Nov 20, 2023
014c1dd
checkpoint
kelloggm Nov 20, 2023
4cd794b
checkpoint, no longer crashing or infinite looping
kelloggm Nov 21, 2023
bbc59fb
handle simple type variables
kelloggm Nov 21, 2023
7ad4b83
cleanup to prep for review
kelloggm Nov 21, 2023
0254a20
remove debug code
kelloggm Nov 21, 2023
89d47ff
failing test
kelloggm Nov 22, 2023
2ffc4d6
address CR comments
kelloggm Nov 22, 2023
620764b
Merge pull request #10 from LoiNguyenCS/main
LoiNguyenCS Nov 27, 2023
5acffa5
remove annotations
LoiNguyenCS Nov 27, 2023
0ea0c7c
add exception for type variables
LoiNguyenCS Nov 27, 2023
78e201f
restore order of expected file in AnonymousClass
LoiNguyenCS Nov 27, 2023
e08e674
Merge pull request #9 from kelloggm/typevar-collision
LoiNguyenCS Nov 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/main/java/org/checkerframework/specimin/SpeciminRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.checkerframework.checker.signature.qual.ClassGetSimpleName;

/** This class is the main runner for Specimin. Use its main() method to start Specimin. */
public class SpeciminRunner {
Expand Down Expand Up @@ -165,7 +166,9 @@ public static void performMinimization(
// correct the types of all related files before adding them to parsedTargetFiles
JavaTypeCorrect typeCorrecter = new JavaTypeCorrect(root, relatedClass);
typeCorrecter.correctTypesForAllFiles();
addMissingClass.updateTypes(typeCorrecter.getTypeToChange());
Map<@ClassGetSimpleName String, @ClassGetSimpleName String> typesToChange =
typeCorrecter.getTypeToChange();
addMissingClass.updateTypes(typesToChange);

for (String directory : relatedClass) {
// directories already in parsedTargetFiles are original files in the root directory, we are
Expand All @@ -187,7 +190,21 @@ public static void performMinimization(
if (isEmptyCompilationUnit(target.getValue())) {
// target key will have this form: "path/of/package/ClassName.java"
String classFullyQualfiedName = target.getKey().replace("/", ".");
classFullyQualfiedName = classFullyQualfiedName.replace(".java", "");
if (!classFullyQualfiedName.endsWith(".java")) {
throw new RuntimeException(
"A Java file directory does not end with .java: " + classFullyQualfiedName);
kelloggm marked this conversation as resolved.
Show resolved Hide resolved
}
classFullyQualfiedName =
classFullyQualfiedName.substring(0, classFullyQualfiedName.length() - 5);
@SuppressWarnings("signature") // since it's the last element of a fully qualified path
@ClassGetSimpleName String simpleName =
classFullyQualfiedName.substring(classFullyQualfiedName.lastIndexOf(".") + 1);
// If this condition is true, this class is a synthetic class initially created to be a
// return type of some synthetic methods, but later javac has found the correct return type
// for that method.
if (typesToChange.containsKey(simpleName)) {
continue;
}
if (!finder.getUsedClass().contains(classFullyQualfiedName)) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.FieldAccessExpr;
Expand Down Expand Up @@ -207,6 +208,28 @@ public Visitable visit(MethodDeclaration method, Void p) {
return result;
}

@Override
public Visitable visit(Parameter para, Void p) {
if (insideTargetMethod) {
ResolvedType paraType = para.resolve().getType();
if (paraType.isReferenceType()) {
String paraTypeFullName =
paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName();
usedClass.add(paraTypeFullName);
for (ResolvedType typeVariable : paraType.asReferenceType().typeParametersValues()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the API, I'd name the loop variable typeParameterValue rather than typeVariable. (I also suggest changing the associated variables, like typeVariableFullName -> typeParameterValueFullName.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also concerned that this code might be overspecialized to the case where classes that have type parameters are used as parameters. Can you add a test case where the target method also defines the type variable in use, such as:

<T> void foo(List<T> list) { ... }

In particular, I'd like a test like this so that we can be certain that this code won't be triggered (creating a class named "T"!) in that case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No extraneous class is created, but Specimin does crash in this case, because it looks for the class T (i.e., a class with the type variable's name). I'll commit the test on this branch. (I don't think I'll have time to actually fix the bug right now, but maybe I'll do that in a bit.)

String typeVariableFullName = typeVariable.describe();
if (typeVariableFullName.contains("<")) {
// removing the "<...>" part if there is any.
typeVariableFullName =
typeVariableFullName.substring(0, typeVariableFullName.indexOf("<"));
}
usedClass.add(typeVariableFullName);
}
}
}
return super.visit(para, p);
}

@Override
public Visitable visit(MethodCallExpr call, Void p) {
if (insideTargetMethod) {
Expand Down
40 changes: 39 additions & 1 deletion src/main/java/org/checkerframework/specimin/UnsolvedClass.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class UnsolvedClass {
*/
private final String packageName;

/** This field records the number of type variables for this class */
private int numberOfTypeVariables = 0;

/**
* Create an instance of UnsolvedClass
*
Expand Down Expand Up @@ -95,6 +98,20 @@ public void addFields(String variableExpression) {
this.classFields.add(variableExpression);
}

/** This method sets the number of type variables for the current class */
public void setNumberOfTypeVariables(int numberOfTypeVariables) {
this.numberOfTypeVariables = numberOfTypeVariables;
}

/**
* This method tells the number of type variables for this class
*
* @return the number of type variables
*/
public int getNumberOfTypeVariables() {
return this.numberOfTypeVariables;
}

/**
* Update the return type of a method. Note: this method is supposed to be used to update
* synthetic methods, where the return type of each method is distinct.
Expand Down Expand Up @@ -149,7 +166,7 @@ public void updateFieldByType(String currentType, String correctType) {
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("package ").append(packageName).append(";\n");
sb.append("public class ").append(className).append(" {\n");
sb.append("public class ").append(className).append(getTypeVariablesAsString()).append(" {\n");
for (String variableDeclarations : classFields) {
sb.append(" " + "public " + variableDeclarations + ";\n");
}
Expand All @@ -159,4 +176,25 @@ public String toString() {
sb.append("}\n");
return sb.toString();
}

/**
* Return a synthetic representation for type variables of the current class.
*
* @return the synthetic representation for type variables
*/
public String getTypeVariablesAsString() {
if (numberOfTypeVariables == 0) {
return "";
}
StringBuilder result = new StringBuilder();
// if class A has three type variables, the expression will be A<T, TT, TTT>
result.append("<");
for (int i = 0; i < numberOfTypeVariables; i++) {
String typeExpression = "T" + "T".repeat(i);
kelloggm marked this conversation as resolved.
Show resolved Hide resolved
result.append(typeExpression).append(", ");
}
result.delete(result.length() - 2, result.length());
result.append(">");
return result.toString();
}
}
123 changes: 117 additions & 6 deletions src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.github.javaparser.ast.stmt.SwitchEntry;
import com.github.javaparser.ast.stmt.TryStmt;
import com.github.javaparser.ast.stmt.WhileStmt;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.type.ReferenceType;
import com.github.javaparser.ast.type.Type;
Expand Down Expand Up @@ -400,13 +401,43 @@ public Visitable visit(BlockStmt node, Void p) {

@Override
public Visitable visit(VariableDeclarator decl, Void p) {
// This part is to update the symbol table.
boolean isAField =
!decl.getParentNode().isEmpty() && (decl.getParentNode().get() instanceof FieldDeclaration);
if (!isAField) {
HashSet<String> currentListOfLocals = localVariables.removeFirst();
currentListOfLocals.add(decl.getNameAsString());
localVariables.addFirst(currentListOfLocals);
}

// This part is to create synthetic class for the type of decl if needed.
Type declType = decl.getType();
try {
declType.resolve();
} catch (UnsolvedSymbolException | UnsupportedOperationException e) {
String typeAsString = declType.asString();
List<String> elements = Splitter.onPattern("\\.").splitToList(typeAsString);
// There could be two cases here: either a fully-qualified class name or a simple class name.
// This is the fully-qualified case.
if (elements.size() > 1) {
@SuppressWarnings("signature") // since this type is in a fully-qualfied form
@FullyQualifiedName String qualifiedTypeName = typeAsString;
updateMissingClass(getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName));
}
/**
* Handles the case where the type is a simple class name. Two sub-cases are considered: 1.
* The class is included among the import statements. 2. The class is not included in the
* import statements but is in the same directory as the input class. The first sub-case is
* addressed by the visit method for ImportDeclaration.
*/
else if (!classAndPackageMap.containsKey(typeAsString)) {
@SuppressWarnings("signature") // since this is the simple name case
@ClassGetSimpleName String className = typeAsString;
String packageName = this.currentPackage;
UnsolvedClass newClass = new UnsolvedClass(className, packageName);
updateMissingClass(newClass);
}
}
return super.visit(decl, p);
}

Expand Down Expand Up @@ -493,7 +524,7 @@ public Visitable visit(MethodDeclaration node, Void arg) {
}
}

HashSet<String> currentLocalVariables = new HashSet<>();
HashSet<String> currentLocalVariables = getParameterFromAMethodDeclaration(node);
localVariables.addFirst(currentLocalVariables);
super.visit(node, arg);
localVariables.removeFirst();
Expand Down Expand Up @@ -541,10 +572,63 @@ public Visitable visit(MethodCallExpr method, Void p) {
return super.visit(method, p);
}

@Override
public Visitable visit(ClassOrInterfaceType typeExpr, Void p) {
/**
* Workaround for a JavaParser bug: When a type is referenced as a class path, like
kelloggm marked this conversation as resolved.
Show resolved Hide resolved
* com.example.Dog dog, JavaParser considers its package components (com and com.example) as
* types, too. This issue happens even when the source file of the Dog class is present in the
* codebase.
*/
if (!isCapital(typeExpr.getName().asString())) {
kelloggm marked this conversation as resolved.
Show resolved Hide resolved
return super.visit(typeExpr, p);
}
if (!typeExpr.isReferenceType()) {
return super.visit(typeExpr, p);
}
try {
typeExpr.getElementType().resolve().describe();
return super.visit(typeExpr, p);
}
/*
* If the class file is not in the codebase yet, we got UnsolvedSymbolException.
* If the class file is not in the codebase and used by an anonymous class, we got UnsupportedOperationException.
* If the class file is in the codebase but the type variables are missing, we got IllegalArgumentException.
*/
catch (UnsolvedSymbolException | UnsupportedOperationException | IllegalArgumentException e) {
// This method only updates type variables for unsolved classes. Other problems causing a
// class to be unsolved will be fixed by other methods.
Optional<NodeList<Type>> typeArguments = typeExpr.getTypeArguments();
UnsolvedClass classToUpdate;
int numberOfArguments = 0;
String typeRawName = typeExpr.getElementType().asString();
if (typeArguments.isPresent()) {
numberOfArguments = typeArguments.get().size();
// without any type argument
typeRawName = typeRawName.substring(0, typeRawName.indexOf("<"));
}
if (isAClassPath(typeRawName)) {
String packageName = typeRawName.substring(0, typeRawName.lastIndexOf("."));
@SuppressWarnings("signature") // since this is the last element of a class path
@ClassGetSimpleName String className = typeRawName.substring(typeRawName.lastIndexOf(".") + 1);
classToUpdate = new UnsolvedClass(className, packageName);
} else {
@SuppressWarnings("signature") // since it is not in a fully qualifed format
@ClassGetSimpleName String className = typeRawName;
String packageName = classAndPackageMap.getOrDefault(className, currentPackage);
classToUpdate = new UnsolvedClass(className, packageName);
}
classToUpdate.setNumberOfTypeVariables(numberOfArguments);
updateMissingClass(classToUpdate);
gotException = true;
}
return super.visit(typeExpr, p);
}

@Override
public Visitable visit(Parameter parameter, Void p) {
try {
parameter.resolve().describeType();
parameter.resolve();
return super.visit(parameter, p);
}
// If the parameter originates from a Java built-in library, such as java.io or java.lang,
Expand Down Expand Up @@ -823,6 +907,21 @@ public static boolean isASuperCall(Expression node) {
}
}

/**
* Given a method declaration, this method will return the set of parameters of that method
* declaration.
*
* @param decl the method declaration
* @return the set of parameters of decl
*/
public HashSet<String> getParameterFromAMethodDeclaration(MethodDeclaration decl) {
HashSet<String> setOfParameters = new HashSet<>();
for (Parameter parameter : decl.getParameters()) {
setOfParameters.add(parameter.getName().asString());
}
return setOfParameters;
}

/**
* For a super call, this method will update the corresponding synthetic class
*
Expand Down Expand Up @@ -1110,6 +1209,9 @@ public void updateMissingClass(UnsolvedClass missedClass) {
for (String variablesDescription : missedClass.getClassFields()) {
e.addFields(variablesDescription);
}
if (missedClass.getNumberOfTypeVariables() > 0) {
e.setNumberOfTypeVariables(missedClass.getNumberOfTypeVariables());
}
return;
}
}
Expand All @@ -1136,9 +1238,10 @@ public void updateSyntheticSourceCode() {
* @param missedClass a synthetic class to be deleted
*/
public void deleteOldSyntheticClass(UnsolvedClass missedClass) {
String classPackage = classAndPackageMap.get(missedClass.getClassName());
String classPackage = missedClass.getPackageName();
String classDirectory = classPackage.replace(".", "/");
String filePathStr =
this.rootDirectory + classPackage + "/" + missedClass.getClassName() + ".java";
this.rootDirectory + classDirectory + "/" + missedClass.getClassName() + ".java";
Path filePath = Path.of(filePathStr);
try {
Files.delete(filePath);
Expand All @@ -1162,6 +1265,7 @@ public void createMissingClass(UnsolvedClass missedClass) {
String filePathStr =
this.rootDirectory + classDirectory + "/" + missedClass.getClassName() + ".java";
Path filePath = Paths.get(filePathStr);

createdClass.add(filePath);
try {
Path parentPath = filePath.getParent();
Expand Down Expand Up @@ -1366,9 +1470,16 @@ public void updateTypes(
+ incorrectType.substring(1).replace("ReturnType", "");
UnsolvedClass relatedClass = syntheticMethodAndClass.get(involvedMethod);
if (relatedClass != null) {
for (UnsolvedClass syntheticClass : missingClass) {
if (syntheticClass.getClassName().equals(relatedClass.getClassName())
&& syntheticClass.getPackageName().equals(relatedClass.getPackageName())) {
syntheticClass.updateMethodByReturnType(
incorrectType, typeToCorrect.get(incorrectType));
this.deleteOldSyntheticClass(syntheticClass);
this.createMissingClass(syntheticClass);
}
}
relatedClass.updateMethodByReturnType(incorrectType, typeToCorrect.get(incorrectType));
this.deleteOldSyntheticClass(relatedClass);
this.createMissingClass(relatedClass);
}
// if the above condition is not met, then this incorrectType is a synthetic type for the
// fields of the parent class rather than the return type of some methods
Expand Down
15 changes: 15 additions & 0 deletions src/test/java/org/checkerframework/specimin/UnsolvedParameter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.checkerframework.specimin;

import java.io.IOException;
import org.junit.Test;

/** This test checks if Specimin can work for tricky, unsolved parameters. */
public class UnsolvedParameter {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"unsolvedparameter",
new String[] {"com/example/Simple.java"},
new String[] {"com.example.Simple#printName(FullName<MiddleName<String>, LastName>)"});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

public class SomeClass {

public SomeClass() {
public int getLocalVar() {
throw new Error();
}

public int getLocalVar() {
public SomeClass() {
throw new Error();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example;

import com.name.FullName;
import com.name.MiddleName;
import com.name.LastName;

public class Simple {

public void printName(FullName<MiddleName<String>, LastName> fullName) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.name;

public class FullName<T, TT> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.name;

public class LastName {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.name;

public class MiddleName<T> {
}
Loading