From 1352279149af22cbe7f380d5ca71af51e7289f27 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 31 Oct 2023 13:37:35 -0400 Subject: [PATCH] add codes for anonymous class --- JavaParser.astub | 1 + .../specimin/MethodPrunerVisitor.java | 17 ++- .../specimin/TargetMethodFinderVisitor.java | 13 ++ .../specimin/UnsolvedSymbolVisitor.java | 126 +++++++++++++----- .../specimin/AnonymousClass.java | 18 +++ .../expected/com/example/Simple.java | 18 +++ .../expected/com/nameless/SomeClass.java | 12 ++ .../input/com/example/Simple.java | 22 +++ 8 files changed, 190 insertions(+), 37 deletions(-) create mode 100644 src/test/java/org/checkerframework/specimin/AnonymousClass.java create mode 100644 src/test/resources/anonymousclass/expected/com/example/Simple.java create mode 100644 src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java create mode 100644 src/test/resources/anonymousclass/input/com/example/Simple.java diff --git a/JavaParser.astub b/JavaParser.astub index 375c823b..8f495e0b 100644 --- a/JavaParser.astub +++ b/JavaParser.astub @@ -1,4 +1,5 @@ import org.checkerframework.checker.signature.qual.*; +import org.checkerframework.checker.nullness.qual.Nullable; package com.github.javaparser.resolution.types; diff --git a/src/main/java/org/checkerframework/specimin/MethodPrunerVisitor.java b/src/main/java/org/checkerframework/specimin/MethodPrunerVisitor.java index 5c3aaf36..1345c020 100644 --- a/src/main/java/org/checkerframework/specimin/MethodPrunerVisitor.java +++ b/src/main/java/org/checkerframework/specimin/MethodPrunerVisitor.java @@ -28,6 +28,12 @@ public class MethodPrunerVisitor extends ModifierVisitor { */ private Set 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 @@ -46,12 +52,19 @@ public MethodPrunerVisitor(Set methodsToKeep, Set 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; } } diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index 42912294..5d099cfa 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -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; @@ -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()); diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 6a2b802c..4486729b 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -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; @@ -78,7 +79,7 @@ public class UnsolvedSymbolVisitor extends ModifierVisitor { private String currentPackage = ""; /** The symbol table to keep track of local variables in the current input file */ - private Stack> localVariables = new Stack<>(); + private final Stack> localVariables = new Stack<>(); /** The simple name of the class currently visited */ private @ClassGetSimpleName String className = ""; @@ -98,7 +99,14 @@ public class UnsolvedSymbolVisitor extends ModifierVisitor { private final Set 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 syntheticMethodAndClass = new HashMap<>(); @@ -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 localVarInCon = new HashSet<>(); localVariables.push(localVarInCon); Expression condition = (Expression) n.getCondition().accept(this, arg); @@ -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 currentListOfLocals = localVariables.pop(); - currentListOfLocals.add(decl.getName().asString()); + currentListOfLocals.add(decl.getNameAsString()); localVariables.push(currentListOfLocals); } return super.visit(decl, p); @@ -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> value = localVariables.iterator(); - while (value.hasNext()) { - if (value.next().contains(fieldName)) { - isALocalVar = true; - break; - } - } - if (!isALocalVar) { + if (!isALocalVar(fieldName)) { updateSyntheticClassForSuperCall(node); } } @@ -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) { @@ -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 currentLocalVariables = new HashSet<>(); localVariables.push(currentLocalVariables); super.visit(node, arg); @@ -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) @@ -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 { @@ -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; } /** @@ -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 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); @@ -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 @@ -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(), "")); @@ -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 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 diff --git a/src/test/java/org/checkerframework/specimin/AnonymousClass.java b/src/test/java/org/checkerframework/specimin/AnonymousClass.java new file mode 100644 index 00000000..cb117db3 --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/AnonymousClass.java @@ -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()"}); + } +} diff --git a/src/test/resources/anonymousclass/expected/com/example/Simple.java b/src/test/resources/anonymousclass/expected/com/example/Simple.java new file mode 100644 index 00000000..46c4942d --- /dev/null +++ b/src/test/resources/anonymousclass/expected/com/example/Simple.java @@ -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; + } +} diff --git a/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java b/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java new file mode 100644 index 00000000..8dd9312d --- /dev/null +++ b/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java @@ -0,0 +1,12 @@ +package com.nameless; + +public class SomeClass { + + public SomeClass() { + throw new Error(); + } + + public int getLocalVar() { + throw new Error(); + } +} diff --git a/src/test/resources/anonymousclass/input/com/example/Simple.java b/src/test/resources/anonymousclass/input/com/example/Simple.java new file mode 100644 index 00000000..e2b351ac --- /dev/null +++ b/src/test/resources/anonymousclass/input/com/example/Simple.java @@ -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; + } +} + + + + +