Skip to content

Commit

Permalink
add codes for anonymous class
Browse files Browse the repository at this point in the history
  • Loading branch information
LoiNguyenCS committed Oct 31, 2023
1 parent b44badd commit 1352279
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 37 deletions.
1 change: 1 addition & 0 deletions JavaParser.astub
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.checkerframework.checker.signature.qual.*;
import org.checkerframework.checker.nullness.qual.Nullable;

package com.github.javaparser.resolution.types;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public class MethodPrunerVisitor extends ModifierVisitor<Void> {
*/
private Set<String> methodsToEmpty;

/**
* This boolean tracks whether the element currently being visited is inside a target method. It
* is set by {@link #visit(MethodDeclaration, Void)}.
*/
private boolean insideTargetMethod = false;

/**
* Creates the pruner. All methods this pruner encounters other than those in its input sets will
* be removed entirely. For both arguments, the Strings should be in the format produced by
Expand All @@ -46,12 +52,19 @@ public MethodPrunerVisitor(Set<String> methodsToKeep, Set<String> methodsToEmpty
public Visitable visit(MethodDeclaration methodDecl, Void p) {
ResolvedMethodDeclaration resolved = methodDecl.resolve();
if (methodsToLeaveUnchanged.contains(resolved.getQualifiedSignature())) {
return super.visit(methodDecl, p);
insideTargetMethod = true;
Visitable result = super.visit(methodDecl, p);
insideTargetMethod = false;
return result;
} else if (methodsToEmpty.contains(resolved.getQualifiedSignature())) {
methodDecl.setBody(StaticJavaParser.parseBlock("{ throw new Error(); }"));
return methodDecl;
} else {
methodDecl.remove();
// if insideTargetMethod is true, this current method declaration belongs to an anonnymous
// class inside the target method.
if (!insideTargetMethod) {
methodDecl.remove();
}
return methodDecl;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.visitor.ModifierVisitor;
import com.github.javaparser.ast.visitor.Visitable;
import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration;
import java.util.ArrayList;
Expand Down Expand Up @@ -179,6 +180,18 @@ public Visitable visit(MethodDeclaration method, Void p) {
// TODO: test this with annotations
String methodName =
this.classFQName + "#" + methodDeclAsString.substring(methodDeclAsString.indexOf(' ') + 1);
// this method belongs to an anonymous class inside the target method
if (insideTargetMethod) {
ObjectCreationExpr parentExpression = (ObjectCreationExpr) method.getParentNode().get();
// since this is a method inside an anonymous class, we can't use getQualifiedSignature()
// directly.
ResolvedConstructorDeclaration resolved = parentExpression.resolve();
String methodPackage = resolved.getPackageName();
String methodClass = resolved.getClassName();
usedMembers.add(methodPackage + "." + methodClass + "." + method.getNameAsString() + "()");
usedClass.add(methodPackage + "." + methodClass);
}

if (this.targetMethodNames.contains(methodName)) {
insideTargetMethod = true;
targetMethods.add(method.resolve().getQualifiedSignature());
Expand Down
126 changes: 91 additions & 35 deletions src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import java.util.Set;
import java.util.Stack;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.ClassGetSimpleName;
import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers;
import org.checkerframework.checker.signature.qual.FullyQualifiedName;
Expand All @@ -78,7 +79,7 @@ public class UnsolvedSymbolVisitor extends ModifierVisitor<Void> {
private String currentPackage = "";

/** The symbol table to keep track of local variables in the current input file */
private Stack<HashSet<String>> localVariables = new Stack<>();
private final Stack<HashSet<String>> localVariables = new Stack<>();

/** The simple name of the class currently visited */
private @ClassGetSimpleName String className = "";
Expand All @@ -98,7 +99,14 @@ public class UnsolvedSymbolVisitor extends ModifierVisitor<Void> {
private final Set<UnsolvedClass> missingClass = new HashSet<>();

/** The same as the root being used in SpeciminRunner */
private String rootDirectory;
private final String rootDirectory;

/**
* This boolean tracks whether the element currently being visited is inside an object creation.
* It is set by {@link #visit(ObjectCreationExpr, Void)}. This boolean helps UnsolvedSymbolVisitor
* recognize anonymous class.
*/
private boolean insideAnObjectCreation = false;

/** This instance maps the name of a synthetic method with its synthetic class */
private final Map<String, UnsolvedClass> syntheticMethodAndClass = new HashMap<>();
Expand Down Expand Up @@ -322,7 +330,8 @@ public Visitable visit(ForStmt node, Void p) {
}

@Override
public Visitable visit(IfStmt n, Void arg) {
@SuppressWarnings("nullness:override")
public @Nullable Visitable visit(IfStmt n, Void arg) {
HashSet<String> localVarInCon = new HashSet<>();
localVariables.push(localVarInCon);
Expression condition = (Expression) n.getCondition().accept(this, arg);
Expand Down Expand Up @@ -404,11 +413,15 @@ public Visitable visit(BlockStmt node, Void p) {

@Override
public Visitable visit(VariableDeclarator decl, Void p) {
// if there is no list of local variables, then the current VariableDeclarator visited is a
// field declaration
if (this.localVariables.size() != 0) {
boolean isAField = false;
if (!decl.getParentNode().isEmpty()) {
if (decl.getParentNode().get() instanceof FieldDeclaration) {
isAField = true;
}
}
if (!isAField) {
HashSet<String> currentListOfLocals = localVariables.pop();
currentListOfLocals.add(decl.getName().asString());
currentListOfLocals.add(decl.getNameAsString());
localVariables.push(currentListOfLocals);
}
return super.visit(decl, p);
Expand All @@ -431,15 +444,7 @@ public Visitable visit(NameExpr node, Void arg) {
if (parentNode.isEmpty()
|| !(parentNode.get() instanceof MethodCallExpr
|| parentNode.get() instanceof FieldAccessExpr)) {
boolean isALocalVar = false;
Iterator<HashSet<String>> value = localVariables.iterator();
while (value.hasNext()) {
if (value.next().contains(fieldName)) {
isALocalVar = true;
break;
}
}
if (!isALocalVar) {
if (!isALocalVar(fieldName)) {
updateSyntheticClassForSuperCall(node);
}
}
Expand Down Expand Up @@ -468,16 +473,13 @@ public Visitable visit(FieldDeclaration node, Void arg) {

@Override
public Visitable visit(MethodDeclaration node, Void arg) {
ClassOrInterfaceDeclaration classNode =
(ClassOrInterfaceDeclaration) node.getParentNode().get();
SimpleName classNodeSimpleName = classNode.getName();
className = classNodeSimpleName.asString();
// a MethodDeclaration instance will have parent node
Node parentNode = node.getParentNode().get();
Type nodeType = node.getType();
// since this is a return type of a method, it is a dot-separated identifier
@SuppressWarnings("signature")
@DotSeparatedIdentifiers String nodeTypeAsString = nodeType.asString();
@ClassGetSimpleName String nodeTypeSimpleForm = toSimpleName(nodeTypeAsString);
methodAndReturnType.put(node.getNameAsString(), nodeTypeSimpleForm);
try {
nodeType.resolve();
} catch (UnsolvedSymbolException | UnsupportedOperationException e) {
Expand All @@ -488,6 +490,24 @@ public Visitable visit(MethodDeclaration node, Void arg) {
this.updateMissingClass(syntheticType);
}

if (!insideAnObjectCreation) {
SimpleName classNodeSimpleName = ((ClassOrInterfaceDeclaration) parentNode).getName();
className = classNodeSimpleName.asString();
methodAndReturnType.put(node.getNameAsString(), nodeTypeSimpleForm);
}
// node is a method declaration inside an anonymous class
else {
try {
// since this method declaration is inside an anonymous class, its parent will be an
// ObjectCreationExpr
((ObjectCreationExpr) parentNode).resolve();
} catch (UnsolvedSymbolException | UnsupportedOperationException e) {
SimpleName classNodeSimpleName = ((ObjectCreationExpr) parentNode).getType().getName();
String nameOfClass = classNodeSimpleName.asString();
updateUnsolvedClassWithMethod(node, nameOfClass, toSimpleName(nodeTypeAsString));
}
}

HashSet<String> currentLocalVariables = new HashSet<>();
localVariables.push(currentLocalVariables);
super.visit(node, arg);
Expand Down Expand Up @@ -521,7 +541,7 @@ public Visitable visit(MethodCallExpr method, Void p) {
updateClassSetWithNotSimpleMethodCall(method);
} else if (calledByAnIncompleteSyntheticClass(method)) {
@ClassGetSimpleName String incompleteClassName = getSyntheticClass(method);
updateUnsolvedClassWithMethodCall(method, incompleteClassName, "");
updateUnsolvedClassWithMethod(method, incompleteClassName, "");
}
this.gotException =
calledByAnUnsolvedSymbol(method)
Expand Down Expand Up @@ -567,7 +587,10 @@ public Visitable visit(ObjectCreationExpr newExpr, Void p) {
if (isFromAJarFile(newExpr)) {
updateClassesFromJarSourcesForObjectCreation(newExpr);
}
return super.visit(newExpr, p);
insideAnObjectCreation = true;
super.visit(newExpr, p);
insideAnObjectCreation = false;
return newExpr;
}
this.gotException = true;
try {
Expand All @@ -579,9 +602,11 @@ public Visitable visit(ObjectCreationExpr newExpr, Void p) {
this.updateMissingClass(newClass);
} catch (Exception q) {
// can not solve the parameters for this object creation in this current run
return super.visit(newExpr, p);
}
return super.visit(newExpr, p);
insideAnObjectCreation = true;
super.visit(newExpr, p);
insideAnObjectCreation = false;
return newExpr;
}

/**
Expand Down Expand Up @@ -637,19 +662,31 @@ public static String setInitialValueForVariableDeclaration(

/**
* This method will add a new method declaration to a synthetic class based on the unsolved method
* call in the original input. User can choose the desired return type for the added method. The
* desired return type can be an empty string, and in that case, Specimin will create another
* synthetic class to be the return type of that method.
* call or method declaration in the original input. User can choose the desired return type for
* the added method. The desired return type can be an empty string, and in that case, Specimin
* will create another synthetic class to be the return type of that method.
*
* @param method the method call in the original input
* @param method the method call or method declaration in the original input
* @param className the name of the synthetic class
* @param desiredReturnType the desired return type for this method
*/
public void updateUnsolvedClassWithMethodCall(
MethodCallExpr method,
public void updateUnsolvedClassWithMethod(
Node method,
@ClassGetSimpleName String className,
@ClassGetSimpleName String desiredReturnType) {
String methodName = method.getNameAsString();
String methodName = "";
List<String> listOfParameters = new ArrayList<>();
if (method instanceof MethodCallExpr) {
methodName = ((MethodCallExpr) method).getNameAsString();
listOfParameters = getArgumentsFromMethodCall(((MethodCallExpr) method));
}
// method is a MethodDeclaration
else {
methodName = ((MethodDeclaration) method).getNameAsString();
for (Parameter para : ((MethodDeclaration) method).getParameters()) {
listOfParameters.add(para.getNameAsString());
}
}
String returnType = "";
if (desiredReturnType.equals("")) {
returnType = returnNameForMethod(methodName);
Expand All @@ -659,11 +696,13 @@ public void updateUnsolvedClassWithMethodCall(
UnsolvedClass missingClass =
new UnsolvedClass(
className, classAndPackageMap.getOrDefault(className, this.chosenPackage));
UnsolvedMethod thisMethod =
new UnsolvedMethod(methodName, returnType, getArgumentsFromMethodCall(method));
UnsolvedMethod thisMethod = new UnsolvedMethod(methodName, returnType, listOfParameters);
missingClass.addMethod(thisMethod);
syntheticMethodAndClass.put(methodName, missingClass);
this.updateMissingClass(missingClass);

// if the return type is not specified, a synthetic return type will be created. This part of
// codes creates the corresponding class for that synthetic return type
if (desiredReturnType.equals("")) {
@SuppressWarnings(
"signature") // returnType is a @ClassGetSimpleName, so combining it with the package will
Expand Down Expand Up @@ -797,7 +836,7 @@ public void updateSyntheticClassForSuperCall(Expression expr) {
"Check if isASuperCall returns true before calling updateSyntheticClassForSuperCall");
}
if (expr instanceof MethodCallExpr) {
updateUnsolvedClassWithMethodCall(
updateUnsolvedClassWithMethod(
expr.asMethodCallExpr(),
getParentClass(className),
methodAndReturnType.getOrDefault(expr.asMethodCallExpr().getNameAsString(), ""));
Expand Down Expand Up @@ -849,6 +888,23 @@ public void updateUnsolvedClassWithFields(
}
}

/**
* This method checks if a variable is local.
*
* @param fieldName the name of the variable
* @return true if that variable is local
*/
public boolean isALocalVar(String fieldName) {
for (HashSet<String> varSet : localVariables) {
// for anonymous classes, it is assumed that any matching local variable either belongs to the
// class itself or is a final variable in the enclosing scope.
if (varSet.contains(fieldName)) {
return true;
}
}
return false;
}

/**
* This method checks if the current run of UnsolvedSymbolVisitor can solve the parameters' types
* of a method call
Expand Down
18 changes: 18 additions & 0 deletions src/test/java/org/checkerframework/specimin/AnonymousClass.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.checkerframework.specimin;

import org.junit.Test;

import java.io.IOException;

/**
* This test checks that if Specimin will work if there is an anonymous class inside the target method
*/
public class AnonymousClass {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"anonymousclass",
new String[] {"com/example/Simple.java"},
new String[] {"com.example.Simple#testAnonymous()"});
}
}
18 changes: 18 additions & 0 deletions src/test/resources/anonymousclass/expected/com/example/Simple.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example;

import com.nameless.SomeClass;

public class Simple {

public int testAnonymous() {
int localVar = 42;
SomeClass myObject = new SomeClass() {

@Override
public int getLocalVar() {
return localVar;
}
};
return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.nameless;

public class SomeClass {

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

public int getLocalVar() {
throw new Error();
}
}
22 changes: 22 additions & 0 deletions src/test/resources/anonymousclass/input/com/example/Simple.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example;

import com.nameless.SomeClass;

public class Simple {
public int testAnonymous() {
int localVar = 42; // An effectively final variable

SomeClass myObject = new SomeClass() {
@Override
public int getLocalVar() {
return localVar;
}
};
return 0;
}
}





0 comments on commit 1352279

Please sign in to comment.