Skip to content

Commit

Permalink
Merge pull request #101 from LoiNguyenCS/preserve-solved-interface
Browse files Browse the repository at this point in the history
Handling interfaces
  • Loading branch information
kelloggm authored Feb 5, 2024
2 parents 5030c22 + a2a1c8a commit 247f98d
Show file tree
Hide file tree
Showing 32 changed files with 417 additions and 4 deletions.
27 changes: 26 additions & 1 deletion src/main/java/org/checkerframework/specimin/PrunerVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
Expand All @@ -16,6 +17,7 @@
import com.github.javaparser.ast.expr.IntegerLiteralExpr;
import com.github.javaparser.ast.expr.LongLiteralExpr;
import com.github.javaparser.ast.expr.NullLiteralExpr;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.visitor.ModifierVisitor;
Expand Down Expand Up @@ -55,6 +57,9 @@ public class PrunerVisitor extends ModifierVisitor<Void> {
*/
private Set<String> classesUsedByTargetMethods;

/** This is to check whether the current compilation unit is a class or an interface. */
private boolean isInsideAnInterface = false;

/**
* This boolean tracks whether the element currently being visited is inside a target method. It
* is set by {@link #visit(MethodDeclaration, Void)}.
Expand Down Expand Up @@ -101,6 +106,24 @@ public Visitable visit(ClassOrInterfaceDeclaration decl, Void p) {
decl.remove();
return decl;
}
if (decl.isInterface()) {
this.isInsideAnInterface = true;
} else {
NodeList<ClassOrInterfaceType> implementedInterfaces = decl.getImplementedTypes();
Iterator<ClassOrInterfaceType> iterator = implementedInterfaces.iterator();
while (iterator.hasNext()) {
ClassOrInterfaceType interfaceType = iterator.next();
try {
String typeFullName = interfaceType.resolve().getQualifiedName();
if (!classesUsedByTargetMethods.contains(typeFullName)) {
iterator.remove();
}
} catch (UnsolvedSymbolException e) {
iterator.remove();
}
}
decl.setImplementedTypes(implementedInterfaces);
}
return super.visit(decl, p);
}

Expand Down Expand Up @@ -129,7 +152,9 @@ public Visitable visit(MethodDeclaration methodDecl, Void p) {
insideTargetMethod = false;
return result;
} else if (membersToEmpty.contains(resolved.getQualifiedSignature())) {
methodDecl.setBody(StaticJavaParser.parseBlock("{ throw new Error(); }"));
if (!isInsideAnInterface) {
methodDecl.setBody(StaticJavaParser.parseBlock("{ throw new Error(); }"));
}
return methodDecl;
} else {
// if insideTargetMethod is true, this current method declaration belongs to an anonnymous
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ public static void performMinimization(
for (String targetFile : targetFiles) {
parsedTargetFiles.put(targetFile, parseJavaFile(root, targetFile));
}
for (String targetFile : addMissingClass.getAddedTargetFiles()) {
parsedTargetFiles.put(targetFile, parseJavaFile(root, targetFile));
}
}

for (CompilationUnit cu : parsedTargetFiles.values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ public class TargetMethodFinderVisitor extends ModifierVisitor<Void> {
*/
private final Map<String, String> importedClassToPackage;

/**
* This map connects the resolved declaration of a method to the interface that contains it, if
* any.
*/
private final Map<ResolvedMethodDeclaration, ClassOrInterfaceType>
methodDeclarationToInterfaceType = new HashMap<>();

/**
* Create a new target method finding visitor.
*
Expand Down Expand Up @@ -149,6 +156,20 @@ public Set<String> getTargetMethods() {
return targetMethods;
}

/**
* Updates the mapping of method declarations to their corresponding interface type based on a
* list of methods and the interface type that contains those methods.
*
* @param methodList the list of resolved method declarations
* @param interfaceType the interface containing the specified methods.
*/
private void updateMethodDeclarationToInterfaceType(
List<ResolvedMethodDeclaration> methodList, ClassOrInterfaceType interfaceType) {
for (ResolvedMethodDeclaration method : methodList) {
this.methodDeclarationToInterfaceType.put(method, interfaceType);
}
}

@Override
public Node visit(ImportDeclaration decl, Void p) {
String classFullName = decl.getNameAsString();
Expand All @@ -163,6 +184,15 @@ public Node visit(ImportDeclaration decl, Void p) {

@Override
public Visitable visit(ClassOrInterfaceDeclaration decl, Void p) {
for (ClassOrInterfaceType interfaceType : decl.getImplementedTypes()) {
try {
updateMethodDeclarationToInterfaceType(
interfaceType.resolve().getAllMethods(), interfaceType);
} catch (UnsolvedSymbolException e) {
continue;
}
}

if (decl.isNestedType()) {
this.classFQName += "." + decl.getName().toString();
} else {
Expand Down Expand Up @@ -226,12 +256,13 @@ public Visitable visit(MethodDeclaration method, Void p) {
}

if (this.targetMethodNames.contains(methodName)) {
ResolvedMethodDeclaration resolvedMethod = method.resolve();
updateUsedClassesForInterface(resolvedMethod);
updateUsedClassWithQualifiedClassName(
method.resolve().getPackageName() + "." + method.resolve().getClassName());
method.resolve().getPackageName() + "." + resolvedMethod.getClassName());
insideTargetMethod = true;
targetMethods.add(method.resolve().getQualifiedSignature());
targetMethods.add(resolvedMethod.getQualifiedSignature());
unfoundMethods.remove(methodName);
ResolvedMethodDeclaration resolvedMethod = method.resolve();
usedClass.add(resolvedMethod.getPackageName() + "." + resolvedMethod.getClassName());
Type returnType = method.getType();
// JavaParser may misinterpret unresolved array types as reference types.
Expand Down Expand Up @@ -430,6 +461,30 @@ public Visitable visit(NameExpr expr, Void p) {
return super.visit(expr, p);
}

/**
* Updates the list of used classes based on a resolved method declaration. If the input method
* originates from an interface, that interface will be added to the list of used classes. The
* determination of whether a method belongs to an interface is based on three criteria: method
* name, method return type, and the number of parameters.
*
* @param method The resolved method declaration to be used for updating the list.
*/
public void updateUsedClassesForInterface(ResolvedMethodDeclaration method) {
for (ResolvedMethodDeclaration interfaceMethod : methodDeclarationToInterfaceType.keySet()) {
if (method.getName().equals(interfaceMethod.getName())) {
String methodReturnType = method.getReturnType().describe();
String interfaceMethodReturnType = interfaceMethod.getReturnType().describe();
if (methodReturnType.equals(interfaceMethodReturnType)) {
if (method.getNumberOfParams() == interfaceMethod.getNumberOfParams()) {
usedClass.add(
methodDeclarationToInterfaceType.get(interfaceMethod).resolve().getQualifiedName());
usedMembers.add(interfaceMethod.getQualifiedSignature());
}
}
}
}
}

/**
* Given a method declaration, this method return the declaration of that method without the
* return type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ public class UnsolvedSymbolVisitor extends ModifierVisitor<Void> {
*/
private final Map<String, @FullyQualifiedName String> staticImportedMembersMap = new HashMap<>();

/** New files that should be added to the list of target files for the next iteration. */
private final Set<String> addedTargetFiles = new HashSet<>();

/**
* Create a new UnsolvedSymbolVisitor instance
*
Expand Down Expand Up @@ -281,6 +284,15 @@ public void setExceptionToFalse() {
gotException = false;
}

/**
* Get the set of target files that should be added for the next iteration.
*
* @return the value of addedTargetFiles.
*/
public Set<String> getAddedTargetFiles() {
return addedTargetFiles;
}

@Override
public Node visit(ImportDeclaration decl, Void arg) {
if (decl.isAsterisk()) {
Expand Down Expand Up @@ -319,6 +331,30 @@ public Visitable visit(ClassOrInterfaceDeclaration node, Void arg) {
addTypeVariableScope(node.getTypeParameters());
Visitable result = super.visit(node, arg);
typeVariables.removeFirst();

NodeList<ClassOrInterfaceType> interfaceList = node.getImplementedTypes();
for (ClassOrInterfaceType interfaceType : interfaceList) {
String qualifiedName =
classAndPackageMap.getOrDefault(className, this.currentPackage)
+ "."
+ interfaceType.getName().asString();
if (classfileIsInOriginalCodebase(qualifiedName)) {
// add the source codes of the interface to the list of target files so that
// UnsolvedSymbolVisitor can solve symbols for that interface if needed.
String filePath = qualifiedName.replace(".", "/");
if (filePath.contains("<")) {
filePath = filePath.substring(filePath.indexOf("<"));
}
filePath = filePath + ".java";
if (!addedTargetFiles.contains(filePath)) {
// strictly speaking, there is no exception here. But we set gotException to true so that
// UnsolvedSymbolVisitor will run at least one more iteration to visit the newly added
// file.
this.gotException = true;
}
addedTargetFiles.add(filePath);
}
}
return result;
}

Expand Down
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 properly remove unused method signatures from an interface. */
public class InterfaceImplementedTest {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"interfaceimplemented",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#doSomething()"});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.checkerframework.specimin;

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

/**
* This test makes sure that Specimin will not crash if methods from interfaces have unsolved return
* types.
*/
public class InterfaceMethodWithUnsolvedTypeTest {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"interfacemethodwithunsolvedtype",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#doSomething(String)"});
}
}
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 interfaces with generic types. */
public class InterfaceWithGenericTypeTest {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"interfacewithgenerictype",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#doSomething(String)"});
}
}
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 makes sure that Specimin will not crash if an interface contains unsolved symbols. */
public class InterfaceWithUnsolvedSymbols {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"interfacewithunsolvedsymbols",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#doSomething(String)"});
}
}
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 properly remove an unsolved interface. */
public class UnsolvedInterfaceTest {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"unsolvedinterface",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#doManyThing()"});
}
}
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 properly remove unused interfaces. */
public class UnusedInterfaceTest {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"unusedinterface",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#doManyThing()"});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example;

public interface Baz {

void doSomething();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example;

class Foo implements Baz {

@Override
public void doSomething() {
System.out.println("Foo is doing something!");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example;

// Baz.java
public interface Baz {
void doSomething();
void doOneThing();
void doNothing();
}

10 changes: 10 additions & 0 deletions src/test/resources/interfaceimplemented/input/com/example/Foo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example;

// Foo.java
class Foo implements Baz {
@Override
public void doSomething() {
System.out.println("Foo is doing something!");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example;

import org.testing.UnsolvedType;

public interface Baz<T> {

UnsolvedType doSomething(T value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example;

import org.testing.UnsolvedType;

class Foo implements Baz<String> {

@Override
public UnsolvedType doSomething(String value) {
System.out.println("Foo is doing something with: " + value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.testing;

public class UnsolvedType {
}
Loading

0 comments on commit 247f98d

Please sign in to comment.