Skip to content

Commit

Permalink
Merge pull request #58 from LoiNguyenCS/minimize-fields
Browse files Browse the repository at this point in the history
Minimize fields
  • Loading branch information
LoiNguyenCS authored Nov 28, 2023
2 parents bd294ff + c14fb6f commit 2bb200e
Show file tree
Hide file tree
Showing 18 changed files with 259 additions and 125 deletions.
109 changes: 0 additions & 109 deletions src/main/java/org/checkerframework/specimin/MethodPrunerVisitor.java

This file was deleted.

184 changes: 184 additions & 0 deletions src/main/java/org/checkerframework/specimin/PrunerVisitor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package org.checkerframework.specimin;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
import com.github.javaparser.ast.expr.CharLiteralExpr;
import com.github.javaparser.ast.expr.DoubleLiteralExpr;
import com.github.javaparser.ast.expr.Expression;
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.PrimitiveType;
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.ResolvedMethodDeclaration;
import java.util.Iterator;
import java.util.Set;

/**
* This visitor removes every member in the compilation unit that is not a member of its {@link
* #methodsToLeaveUnchanged} set or {@link #membersToEmpty} set. It also deletes the bodies of all
* methods and replaces them with "throw new Error();" or remove the initializers of fields
* (minimized if the field is final) within the {@link #membersToEmpty} set.
*/
public class PrunerVisitor extends ModifierVisitor<Void> {

/**
* The methods that should NOT be touched by this pruner. The strings representing the method are
* those returned by ResolvedMethodDeclaration#getQualifiedSignature.
*/
private Set<String> methodsToLeaveUnchanged;

/**
* The members, fields and methods, to be pruned. For methods, the bodies are removed. For fields,
* the initializers are removed, or minimized in case the field is final. The strings representing
* the method are those returned by ResolvedMethodDeclaration#getQualifiedSignature. The strings
* representing the field are returned by ResolvedTypeDeclaration#getQualifiedName.
*/
private Set<String> membersToEmpty;

/**
* This is the set of classes used by the target methods. We use this set to determine if we
* should keep or delete an import statement. The strings representing the classes are in
* the @FullyQualifiedName form.
*/
private Set<String> classesUsedByTargetMethods;

/**
* 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 members this pruner encounters other than those in its input sets will
* be removed entirely. For methods in both arguments, the Strings should be in the format
* produced by ResolvedMethodDeclaration#getQualifiedSignature. For fields in {@link
* #membersToEmpty}, the Strings should be in the format produced by
* ResolvedTypeDeclaration#getQualifiedName.
*
* @param methodsToKeep the set of methods whose bodies should be kept intact (usually the target
* methods for specimin)
* @param membersToEmpty the set of members that this pruner will empty
* @param classesUsedByTargetMethods the classes used by target methods
*/
public PrunerVisitor(
Set<String> methodsToKeep,
Set<String> membersToEmpty,
Set<String> classesUsedByTargetMethods) {
this.methodsToLeaveUnchanged = methodsToKeep;
this.membersToEmpty = membersToEmpty;
this.classesUsedByTargetMethods = classesUsedByTargetMethods;
}

@Override
public Node visit(ImportDeclaration decl, Void p) {
String classFullName = decl.getNameAsString();
if (classesUsedByTargetMethods.contains(classFullName)) {
return super.visit(decl, p);
}
decl.remove();
return decl;
}

@Override
public Visitable visit(MethodDeclaration methodDecl, Void p) {
ResolvedMethodDeclaration resolved = methodDecl.resolve();
if (methodsToLeaveUnchanged.contains(resolved.getQualifiedSignature())) {
insideTargetMethod = true;
Visitable result = super.visit(methodDecl, p);
insideTargetMethod = false;
return result;
} else if (membersToEmpty.contains(resolved.getQualifiedSignature())) {
methodDecl.setBody(StaticJavaParser.parseBlock("{ throw new Error(); }"));
return methodDecl;
} else {
// if insideTargetMethod is true, this current method declaration belongs to an anonnymous
// class inside the target method.
if (!insideTargetMethod) {
methodDecl.remove();
}
return methodDecl;
}
}

@Override
public Visitable visit(ConstructorDeclaration constructorDecl, Void p) {
ResolvedConstructorDeclaration resolved = constructorDecl.resolve();
if (methodsToLeaveUnchanged.contains(resolved.getQualifiedSignature())) {
return super.visit(constructorDecl, p);
} else if (membersToEmpty.contains(resolved.getQualifiedSignature())) {
constructorDecl.setBody(StaticJavaParser.parseBlock("{ throw new Error(); }"));
return constructorDecl;
} else {
constructorDecl.remove();
return constructorDecl;
}
}

@Override
public Visitable visit(FieldDeclaration fieldDecl, Void p) {
String classFullName = fieldDecl.resolve().declaringType().getQualifiedName();
boolean isFinal = fieldDecl.isFinal();
Iterator<VariableDeclarator> iterator = fieldDecl.getVariables().iterator();
while (iterator.hasNext()) {
VariableDeclarator varDecl = iterator.next();
String varFullName = classFullName + "#" + varDecl.getNameAsString();

if (membersToEmpty.contains(varFullName)) {
varDecl.removeInitializer();
if (isFinal) {
varDecl.setInitializer(getBasicInitializer(varDecl.getType()));
}
} else {
iterator.remove();
}
}

return super.visit(fieldDecl, p);
}

/**
* Creates a basic initializer expression for a specified field type. The way the initial value is
* chosen is based on the document of the Java Language:
* https://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5
*
* @param fieldType The type for which to generate the basic initializer.
* @return An Expression representing the basic initializer for the given field type.
*/
private Expression getBasicInitializer(Type fieldType) {
if (fieldType.isPrimitiveType()) {
PrimitiveType.Primitive primitiveType = ((PrimitiveType) fieldType).getType();
switch (primitiveType) {
case BOOLEAN:
return new BooleanLiteralExpr(false);
case INT:
return new IntegerLiteralExpr("0");
case LONG:
return new LongLiteralExpr("0L");
case FLOAT:
return new DoubleLiteralExpr("0.0f");
case DOUBLE:
return new DoubleLiteralExpr("0.0");
case BYTE:
return new IntegerLiteralExpr("0");
case SHORT:
return new IntegerLiteralExpr("0");
case CHAR:
return new CharLiteralExpr("'\u0000'");
default:
throw new RuntimeException("Unexpected primitive type: " + fieldType);
}
} else {
return new NullLiteralExpr();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ public static void performMinimization(
}
}

MethodPrunerVisitor methodPruner =
new MethodPrunerVisitor(
PrunerVisitor methodPruner =
new PrunerVisitor(
finder.getTargetMethods(), finder.getUsedMembers(), finder.getUsedClass());

for (CompilationUnit cu : parsedTargetFiles.values()) {
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.UnsolvedSymbolException;
import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration;
Expand Down Expand Up @@ -272,7 +273,18 @@ public Visitable visit(ExplicitConstructorInvocationStmt expr, Void p) {
@Override
public Visitable visit(FieldAccessExpr expr, Void p) {
if (insideTargetMethod) {
usedMembers.add(classFQName + "#" + expr.getName().asString());
String fullNameOfClass;
try {
// while the name of the method is declaringType(), it actually returns the class where the
// field is declared
fullNameOfClass = expr.resolve().asField().declaringType().getQualifiedName();
usedMembers.add(fullNameOfClass + "#" + expr.getName().asString());
usedClass.add(fullNameOfClass);
} catch (UnsolvedSymbolException e) {
// if the a field is accessed in the form of a fully-qualified path, such as
// org.example.A.b, then other components in the path apart from the class name and field
// name, such as org and org.example, will also be considered as FieldAccessExpr.
}
}
Expression caller = expr.getScope();
if (caller instanceof SuperExpr) {
Expand All @@ -289,7 +301,11 @@ public Visitable visit(NameExpr expr, Void p) {
|| parentNode.get() instanceof FieldAccessExpr)) {
ResolvedValueDeclaration exprDecl = expr.resolve();
if (exprDecl instanceof ResolvedFieldDeclaration) {
usedClass.add(exprDecl.asField().declaringType().getQualifiedName());
// while the name of the method is declaringType(), it actually returns the class where the
// field is declared
String classFullName = exprDecl.asField().declaringType().getQualifiedName();
usedClass.add(classFullName);
usedMembers.add(classFullName + "#" + expr.getNameAsString());
}
}
return super.visit(expr, p);
Expand Down
15 changes: 15 additions & 0 deletions src/test/java/org/checkerframework/specimin/FieldToEmptyTest.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 remove unused fields and minimized used fields. */
public class FieldToEmptyTest {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"fieldtoempty",
new String[] {"com/example/Simple.java"},
new String[] {"com.example.Simple#test()"});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public class Parent {

public sample.pack.MyType theName = null;
public sample.pack.MyType theName;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.example;

public class Baz {
static int x = 5;
static int x;
}
13 changes: 13 additions & 0 deletions src/test/resources/fieldtoempty/expected/com/example/Simple.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example;

class Simple {

final int b = 0;

int c;

void test() {
c++;
b++;
}
}
Loading

0 comments on commit 2bb200e

Please sign in to comment.