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

Handling interfaces #101

Merged
merged 5 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -142,6 +142,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.
LoiNguyenCS marked this conversation as resolved.
Show resolved Hide resolved
*
* @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
Loading