diff --git a/src/main/java/org/checkerframework/specimin/AnnotationParameterTypesVisitor.java b/src/main/java/org/checkerframework/specimin/AnnotationParameterTypesVisitor.java index 7fec24b5f..bd9cfcc92 100644 --- a/src/main/java/org/checkerframework/specimin/AnnotationParameterTypesVisitor.java +++ b/src/main/java/org/checkerframework/specimin/AnnotationParameterTypesVisitor.java @@ -28,8 +28,7 @@ */ public class AnnotationParameterTypesVisitor extends SpeciminStateVisitor { /** - * Constructs a new SolveMethodOverridingVisitor with the provided sets of target methods, used - * members, and used classes. + * Constructs a new AnnotationParameterTypesVisitor with the previous visitor * * @param previousVisitor the last visitor to run before this one */ diff --git a/src/main/java/org/checkerframework/specimin/JavaParserUtil.java b/src/main/java/org/checkerframework/specimin/JavaParserUtil.java index 0962d6560..2c3fb767f 100644 --- a/src/main/java/org/checkerframework/specimin/JavaParserUtil.java +++ b/src/main/java/org/checkerframework/specimin/JavaParserUtil.java @@ -54,6 +54,32 @@ public static void removeNode(Node node) { } } + /** + * This method checks if a string has the form of a class path. + * + * @param potentialClassPath the string to be checked + * @return true if the string is a class path + */ + public static boolean isAClassPath(String potentialClassPath) { + List elements = Splitter.onPattern("\\.").splitToList(potentialClassPath); + int elementsCount = elements.size(); + return elementsCount > 1 + && isCapital(elements.get(elementsCount - 1)) + // Classpaths cannot contain spaces! + && elements.stream().noneMatch(s -> s.contains(" ")); + } + + /** + * This method checks if a string is capitalized + * + * @param string the string to be checked + * @return true if the string is capitalized + */ + public static boolean isCapital(String string) { + Character first = string.charAt(0); + return Character.isUpperCase(first); + } + /** * Utility method to check if the given declaration is a local class declaration. * diff --git a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java index 3a77c32ca..daf3c70c2 100644 --- a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java +++ b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java @@ -500,6 +500,8 @@ private static void performMinimizationImpl( cu.accept(methodPruner, null); } + removeUnusedImports(parsedTargetFiles); + // cache to avoid called Files.createDirectories repeatedly with the same arguments Set createdDirectories = new HashSet<>(); Set targetFilesAbsolutePaths = new HashSet<>(); @@ -647,6 +649,20 @@ private static SpeciminStateVisitor processAnnotationTypes( return annotationParameterTypesVisitor; } + /** + * Removes all unused imports in each output file through {@code UnusedImportRemoverVisitor}. + * + * @param parsedTargetFiles the files to remove unused imports + */ + private static void removeUnusedImports(Map parsedTargetFiles) { + UnusedImportRemoverVisitor unusedImportRemover = new UnusedImportRemoverVisitor(); + + for (CompilationUnit cu : parsedTargetFiles.values()) { + cu.accept(unusedImportRemover, null); + unusedImportRemover.removeUnusedImports(); + } + } + /** * Helper method to create a human-readable table of the unfound members and each member in the * same class that was considered. diff --git a/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java index 6f08f348e..f349d3cab 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java @@ -514,7 +514,7 @@ public Visitable visit(MethodCallExpr call, Void p) { // a call to a fully-qualified static method) or the scope is a simple name. // In the simple name case, append the current package to the front, since // if it had been imported we wouldn't be in this situation. - if (UnsolvedSymbolVisitor.isAClassPath(scopeAsString)) { + if (JavaParserUtil.isAClassPath(scopeAsString)) { resolvedYetStuckMethodCall.add(scopeAsString + "." + call.getNameAsString()); usedTypeElements.add(scopeAsString); } else { @@ -869,7 +869,7 @@ public static void updateUsedClassWithQualifiedClassName( String potentialOuterClass = qualifiedClassName.substring(0, qualifiedClassName.lastIndexOf(".")); - if (UnsolvedSymbolVisitor.isAClassPath(potentialOuterClass)) { + if (JavaParserUtil.isAClassPath(potentialOuterClass)) { updateUsedClassWithQualifiedClassName( potentialOuterClass, usedTypeElement, nonPrimaryClassesToPrimaryClass); } diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedAnnotationRemoverVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedAnnotationRemoverVisitor.java index 263c66d50..7224d4db9 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedAnnotationRemoverVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedAnnotationRemoverVisitor.java @@ -114,7 +114,7 @@ public void processAnnotations(AnnotationExpr annotation) { isResolved = false; } - if (!UnsolvedSymbolVisitor.isAClassPath(annotationName)) { + if (!JavaParserUtil.isAClassPath(annotationName)) { if (!classToFullClassName.containsKey(annotationName)) { // An annotation not imported and from the java.lang package is not our concern. if (!JavaLangUtils.isJavaLangName(annotationName)) { diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedClassOrInterface.java b/src/main/java/org/checkerframework/specimin/UnsolvedClassOrInterface.java index b5fb5b074..b62de9cec 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedClassOrInterface.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedClassOrInterface.java @@ -324,7 +324,7 @@ public boolean extend(String targetTypeName, String extendsName, UnsolvedSymbolV || "java.lang.annotation.Annotation".equals(extendsName)) { setIsAnAnnotationToTrue(); } else { - if (!UnsolvedSymbolVisitor.isAClassPath(extendsName)) { + if (!JavaParserUtil.isAClassPath(extendsName)) { extendsName = visitor.getPackageFromClassName(extendsName) + "." + extendsName; } extend(extendsName); diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 071eabe9a..b925616c6 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -1086,7 +1086,7 @@ public Visitable visit(FieldAccessExpr node, Void p) { } catch (UnsolvedSymbolException | UnsupportedOperationException e) { // for a qualified name field access such as org.sample.MyClass.field, org.sample will also be // considered FieldAccessExpr. - if (isAClassPath(node.getScope().toString())) { + if (JavaParserUtil.isAClassPath(node.getScope().toString())) { gotException(); } } @@ -1101,7 +1101,7 @@ public Visitable visit(MethodReferenceExpr node, Void p) { if (scope.isTypeExpr()) { Type scopeAsType = scope.asTypeExpr().getType(); String scopeAsTypeFQN = scopeAsType.asString(); - if (!isAClassPath(scopeAsTypeFQN) && scopeAsType.isClassOrInterfaceType()) { + if (!JavaParserUtil.isAClassPath(scopeAsTypeFQN) && scopeAsType.isClassOrInterfaceType()) { scopeAsTypeFQN = getQualifiedNameForClassOrInterfaceType(scopeAsType.asClassOrInterfaceType()); } @@ -1219,7 +1219,7 @@ public Visitable visit(ClassOrInterfaceType typeExpr, Void p) { // like com.example.Dog dog, JavaParser considers its package components (com and com.example) // as types, too. This issue happens even when the source file of the Dog class is present in // the codebase. - if (!isCapital(typeExpr.getName().asString())) { + if (!JavaParserUtil.isCapital(typeExpr.getName().asString())) { return super.visit(typeExpr, p); } // type belonging to a class declaration will be handled by the visit method for @@ -1416,7 +1416,7 @@ Test foo() { UnsolvedClassOrInterface unsolvedAnnotation; - if (isAClassPath(anno.getNameAsString())) { + if (JavaParserUtil.isAClassPath(anno.getNameAsString())) { @SuppressWarnings("signature") // Already guaranteed to be a FQN here @FullyQualifiedName String qualifiedTypeName = anno.getNameAsString(); unsolvedAnnotation = getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName); @@ -1463,7 +1463,7 @@ Test foo() { UnsolvedClassOrInterface unsolvedAnnotation; - if (isAClassPath(anno.getNameAsString())) { + if (JavaParserUtil.isAClassPath(anno.getNameAsString())) { @SuppressWarnings("signature") // Already guaranteed to be a FQN here @FullyQualifiedName String qualifiedTypeName = anno.getNameAsString(); unsolvedAnnotation = getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName); @@ -1520,7 +1520,7 @@ Test foo() { UnsolvedClassOrInterface unsolvedAnnotation; - if (isAClassPath(anno.getNameAsString())) { + if (JavaParserUtil.isAClassPath(anno.getNameAsString())) { @SuppressWarnings("signature") // Already guaranteed to be a FQN here @FullyQualifiedName String qualifiedTypeName = anno.getNameAsString(); unsolvedAnnotation = getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName); @@ -1712,12 +1712,13 @@ private void solveSymbolsForClassOrInterfaceType( } String packageName, className; - if (isAClassPath(typeRawName)) { + if (JavaParserUtil.isAClassPath(typeRawName)) { // Two cases: this could be either an Outer.Inner pair or it could // be a fully-qualified name. If it's an Outer.Inner pair, we identify // that via the heuristic that there are only two elements if we split on // the dot and that the whole string is capital - if (typeRawName.indexOf('.') == typeRawName.lastIndexOf('.') && isCapital(typeRawName)) { + if (typeRawName.indexOf('.') == typeRawName.lastIndexOf('.') + && JavaParserUtil.isCapital(typeRawName)) { className = typeRawName; packageName = getPackageFromClassName(typeRawName.substring(0, typeRawName.indexOf('.'))); } else { @@ -2529,7 +2530,7 @@ public String getQualifiedNameForClassOrInterfaceType(ClassOrInterfaceType type) if (splitType.size() > 2) { // if the above conditions are met, this type is probably already in the qualified form. return typeAsString; - } else if (isCapital(typeAsString)) { + } else if (JavaParserUtil.isCapital(typeAsString)) { // Heuristic: if the type name has two dot-separated components and // the first one is capitalized, then it's probably an inner class. // Return the outer class' package. @@ -2911,7 +2912,7 @@ public void updateMissingClass(UnsolvedClassOrInterface missedClass) { // name might be "java.util" and the class name might be "Map.Entry". String outerClassName = null, innerClassName = null; // First case, looking for "Map.Entry" pattern - if (isCapital(qualifiedName) + if (JavaParserUtil.isCapital(qualifiedName) && // This test checks that it has only one . qualifiedName.indexOf('.') == qualifiedName.lastIndexOf('.')) { @@ -3073,32 +3074,6 @@ public static String toCapital(String string) { return Ascii.toUpperCase(string.substring(0, 1)) + string.substring(1); } - /** - * This method checks if a string is capitalized - * - * @param string the string to be checked - * @return true if the string is capitalized - */ - public static boolean isCapital(String string) { - Character first = string.charAt(0); - return Character.isUpperCase(first); - } - - /** - * This method checks if a string has the form of a class path. - * - * @param potentialClassPath the string to be checked - * @return true if the string is a class path - */ - public static boolean isAClassPath(String potentialClassPath) { - List elements = Splitter.onPattern("\\.").splitToList(potentialClassPath); - int elementsCount = elements.size(); - return elementsCount > 1 - && isCapital(elements.get(elementsCount - 1)) - // Classpaths cannot contain spaces! - && elements.stream().noneMatch(s -> s.contains(" ")); - } - /** * Given the name of a class in the @FullyQualifiedName, this method will create a synthetic class * for that class @@ -3108,9 +3083,9 @@ && isCapital(elements.get(elementsCount - 1)) */ public static UnsolvedClassOrInterface getSimpleSyntheticClassFromFullyQualifiedName( @FullyQualifiedName String fullyName) { - if (!isAClassPath(fullyName)) { + if (!JavaParserUtil.isAClassPath(fullyName)) { throw new RuntimeException( - "Check with isAClassPath first before using" + "Check with JavaParserUtil.isAClassPath first before using" + " getSimpleSyntheticClassFromFullyQualifiedName. Non-classpath-like name: " + fullyName); } @@ -3212,7 +3187,7 @@ public boolean isAnUnsolvedStaticMethodCalledByAQualifiedClassName(MethodCallExp return false; } String callerToString = callerExpression.get().toString(); - return isAClassPath(callerToString); + return JavaParserUtil.isAClassPath(callerToString); } } @@ -3227,7 +3202,7 @@ public boolean isAnUnsolvedStaticMethodCalledByAQualifiedClassName(MethodCallExp */ public boolean isAQualifiedFieldSignature(String field) { String caller = field.substring(0, field.lastIndexOf(".")); - return isAClassPath(caller); + return JavaParserUtil.isAClassPath(caller); } /** @@ -3655,7 +3630,7 @@ private void lookupTypeArgumentFQN(StringBuilder fullyQualifiedName, Type typeAr fullyQualifiedName.append("? extends "); lookupTypeArgumentFQN(fullyQualifiedName, asWildcardType.getExtendedType().get()); } - } else if (isAClassPath(erased)) { + } else if (JavaParserUtil.isAClassPath(erased)) { // If it's already a fully qualified name, don't do anything fullyQualifiedName.append(typeArgument.asString()); } else { diff --git a/src/main/java/org/checkerframework/specimin/UnusedImportRemoverVisitor.java b/src/main/java/org/checkerframework/specimin/UnusedImportRemoverVisitor.java new file mode 100644 index 000000000..0ea880357 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/UnusedImportRemoverVisitor.java @@ -0,0 +1,292 @@ +package org.checkerframework.specimin; + +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.MarkerAnnotationExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.NormalAnnotationExpr; +import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +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.ResolvedEnumConstantDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Removes all unused import statements from a compilation unit. This visitor should be used after + * pruning. + */ +public class UnusedImportRemoverVisitor extends ModifierVisitor { + /** + * A Map of fully qualified type/member names, or a wildcard import, to the actual import + * declaration itself + */ + private final Map typeNamesToImports = new HashMap<>(); + + /** A map of fully qualified imports to their simple import names */ + private final Map fullyQualifiedImportsToSimple = new HashMap<>(); + + /** A set of all fully qualified type/member names in the current compilation unit */ + private final Set usedImports = new HashSet<>(); + + /** A set of unsolvable member names. */ + private final Set unsolvedMembers = new HashSet<>(); + + /** The package of the current compilation unit. */ + private String currentPackage = ""; + + /** + * Removes unused imports from the current compilation unit and resets the state to be used with + * another compilation unit. + */ + public void removeUnusedImports() { + for (Map.Entry entry : typeNamesToImports.entrySet()) { + if (!usedImports.contains(entry.getKey())) { + // In special cases (namely with MethodCallExprs containing lambdas), JavaParser can have + // trouble resolving it, so we should preserve its imports through approximation by simple + // method names. + if (!unsolvedMembers.isEmpty()) { + String simpleName = fullyQualifiedImportsToSimple.get(entry.getKey()); + + if (simpleName != null && unsolvedMembers.contains(simpleName)) { + continue; + } + } + entry.getValue().remove(); + } else if (!currentPackage.equals("") + && entry.getKey().startsWith(currentPackage + ".") + && !entry.getKey().substring(currentPackage.length() + 1).contains(".")) { + // If importing a class from the same package, remove the unnecessary import + entry.getValue().remove(); + } + } + + typeNamesToImports.clear(); + usedImports.clear(); + fullyQualifiedImportsToSimple.clear(); + unsolvedMembers.clear(); + currentPackage = ""; + } + + @Override + public Node visit(ImportDeclaration decl, Void arg) { + String importName = decl.getNameAsString(); + + // ImportDeclaration does not contain the asterisk by default; we need to add it + if (decl.isAsterisk()) { + importName += ".*"; + } else { + String className = importName.substring(0, importName.lastIndexOf(".")); + String elementName = importName.replace(className + ".", ""); + fullyQualifiedImportsToSimple.put(importName, elementName); + } + + typeNamesToImports.put(importName, decl); + return super.visit(decl, arg); + } + + @Override + public Visitable visit(PackageDeclaration node, Void arg) { + currentPackage = node.getNameAsString(); + + return super.visit(node, arg); + } + + @Override + public Visitable visit(ClassOrInterfaceType type, Void arg) { + // Workaround for a JavaParser bug: see UnsolvedSymbolVisitor#visit(ClassOrInterfaceType) + // Also, if it's already fully qualified, it's not tied to an import + if (!JavaParserUtil.isCapital(type.getName().asString()) + || JavaParserUtil.isAClassPath(type.getName().asString())) { + return super.visit(type, arg); + } + + String fullyQualified; + try { + fullyQualified = JavaParserUtil.erase(type.resolve().describe()); + } catch (UnsolvedSymbolException ex) { + // Specimin made an error somewhere if this type is unresolvable; + // TODO: fix this once MethodReturnFullyQualifiedGenericTest is fixed + return super.visit(type, arg); + } + + if (!fullyQualified.contains(".")) { + // Type variable; definitely not imported + return super.visit(type, arg); + } + + String wildcard = fullyQualified.substring(0, fullyQualified.lastIndexOf('.')) + ".*"; + + // Check include both the fully qualified name and the wildcard to match a potential import + // e.g. java.util.List and java.util.* + usedImports.add(fullyQualified); + usedImports.add(wildcard); + return super.visit(type, arg); + } + + @Override + public Visitable visit(FieldAccessExpr expr, Void arg) { + if (expr.hasScope()) { + handleScopeExpression(expr.getScope()); + } + return super.visit(expr, arg); + } + + @Override + public Visitable visit(NameExpr expr, Void arg) { + if (expr.getParentNode().isPresent()) { + // If it's a field access/method call expression, other methods will handle this + if (expr.getParentNode().get() instanceof FieldAccessExpr) { + return super.visit(expr, arg); + } + if (expr.getParentNode().get() instanceof MethodCallExpr) { + // visit(MethodCallExpr) only handles it if it's the scope + MethodCallExpr parent = (MethodCallExpr) expr.getParentNode().get(); + if (parent.hasScope() && parent.getScope().get().toString().equals(expr.toString())) { + return super.visit(expr, arg); + } + } + } + + ResolvedValueDeclaration resolved = expr.resolve(); + + // Handle statically imported fields + // e.g. + // import static java.lang.Math.PI; + // double x = PI; + // ^^ + if (resolved.isField()) { + ResolvedFieldDeclaration asField = resolved.asField(); + String declaringType = JavaParserUtil.erase(asField.declaringType().getQualifiedName()); + // Check for both cases, e.g.: java.lang.Math.PI and java.lang.Math.* + usedImports.add(declaringType + "." + asField.getName()); + usedImports.add(declaringType + ".*"); + } else if (resolved.isEnumConstant()) { + ResolvedEnumConstantDeclaration asEnumConstant = resolved.asEnumConstant(); + String declaringType = JavaParserUtil.erase(asEnumConstant.getType().describe()); + // Importing the enum itself, a static import of a specific enum value, or a wildcard + // for all values of the enum + // e.g. + // com.example.Enum, com.example.Enum.VALUE, com.example.Enum.* + usedImports.add(declaringType); + usedImports.add(declaringType + "." + asEnumConstant.getName()); + usedImports.add(declaringType + ".*"); + } + return super.visit(expr, arg); + } + + @Override + public Visitable visit(MethodCallExpr expr, Void arg) { + ResolvedMethodDeclaration resolved; + try { + resolved = expr.resolve(); + } catch (UnsupportedOperationException ex) { + // Lambdas can raise an UnsupportedOperationException + return super.visit(expr, arg); + } catch (UnsolvedSymbolException | IllegalStateException ex) { + unsolvedMembers.add(expr.getNameAsString()); + return super.visit(expr, arg); + } + + if (resolved.isStatic()) { + // If it has a scope, the parent class is imported + if (expr.hasScope()) { + handleScopeExpression(expr.getScope().get()); + } else { + // Handle statically imported methods + // e.g. + // import static java.lang.Math.sqrt; + // sqrt(1); + // Check for qualified name and the wildcard, e.g.: java.lang.Math.sqrt and java.lang.Math.* + usedImports.add(JavaParserUtil.erase(resolved.getQualifiedName())); + usedImports.add(JavaParserUtil.erase(resolved.declaringType().getQualifiedName()) + ".*"); + } + } + + return super.visit(expr, arg); + } + + @Override + public Visitable visit(MarkerAnnotationExpr anno, Void arg) { + handleAnnotation(anno); + + return super.visit(anno, arg); + } + + @Override + public Visitable visit(NormalAnnotationExpr anno, Void arg) { + handleAnnotation(anno); + + return super.visit(anno, arg); + } + + @Override + public Visitable visit(SingleMemberAnnotationExpr anno, Void arg) { + handleAnnotation(anno); + + return super.visit(anno, arg); + } + + /** + * Helper method to handle the scope type in a FieldAccessExpr or MethodCallExpr. + * + * @param scope The scope as an Expression + */ + private void handleScopeExpression(Expression scope) { + // Workaround for a JavaParser bug: see UnsolvedSymbolVisitor#visit(ClassOrInterfaceType) + if (!JavaParserUtil.isCapital(scope.toString())) { + return; + } + + String fullyQualified = JavaParserUtil.erase(scope.calculateResolvedType().describe()); + + if (!fullyQualified.contains(".")) { + // If there is no ., it is not a class (e.g. this.values.length) + return; + } + + String wildcard = getWildcardFromClassOrMemberName(fullyQualified); + + usedImports.add(fullyQualified); + usedImports.add(wildcard); + } + + /** + * Helper method to resolve all annotation expressions and add them to usedImports. + * + * @param anno The annotation expression to handle + */ + private void handleAnnotation(AnnotationExpr anno) { + String fullyQualified = JavaParserUtil.erase(anno.resolve().getQualifiedName()); + String wildcard = getWildcardFromClassOrMemberName(fullyQualified); + + // Check for the fully qualified class name, or a wildcard import of the annotation's package + // e.g. java.lang.annotation.Target and java.lang.annotation.* + usedImports.add(fullyQualified); + usedImports.add(wildcard); + } + + /** + * Helper method to convert a fully qualified class/member name into a wildcard e.g. + * {@code java.lang.Math.sqrt} --> {@code java.lang.Math.*} + * + * @param fullyQualified The fully qualified name + * @return {@code fullyQualified} with the text after the last dot replaced with an + * asterisk ({@code *}) + */ + private static String getWildcardFromClassOrMemberName(String fullyQualified) { + return fullyQualified.substring(0, fullyQualified.lastIndexOf('.')) + ".*"; + } +} diff --git a/src/test/java/org/checkerframework/specimin/UnsovledSymbolVisitorStaticTest.java b/src/test/java/org/checkerframework/specimin/JavaParserUtilIsAClassPathTest.java similarity index 65% rename from src/test/java/org/checkerframework/specimin/UnsovledSymbolVisitorStaticTest.java rename to src/test/java/org/checkerframework/specimin/JavaParserUtilIsAClassPathTest.java index e687672f8..95ae9eb25 100644 --- a/src/test/java/org/checkerframework/specimin/UnsovledSymbolVisitorStaticTest.java +++ b/src/test/java/org/checkerframework/specimin/JavaParserUtilIsAClassPathTest.java @@ -5,8 +5,8 @@ import org.junit.Test; -/** This class unit tests static methods in UnsolvedSymbolVisitor. */ -public class UnsovledSymbolVisitorStaticTest { +/** This class unit tests the isAClassPath method in JavaParserUtil. */ +public class JavaParserUtilIsAClassPathTest { private static final String LONG_CHAIN = "BigQueryIO.read(BillingEvent::parseFromRecord)\n" @@ -42,20 +42,20 @@ public class UnsovledSymbolVisitorStaticTest { @Test public void testIsAClassPath() { - assertTrue(UnsolvedSymbolVisitor.isAClassPath("org.checkerframework.javacutil.ElementUtils")); - - assertFalse(UnsolvedSymbolVisitor.isAClassPath("org")); - assertFalse(UnsolvedSymbolVisitor.isAClassPath("ElementUtils")); - assertFalse(UnsolvedSymbolVisitor.isAClassPath("ElementUtils.foo()")); - assertFalse(UnsolvedSymbolVisitor.isAClassPath("org.ElementUtils.foo()")); - assertFalse(UnsolvedSymbolVisitor.isAClassPath("MapElements.into(TypeDescriptors.strings())")); - assertFalse(UnsolvedSymbolVisitor.isAClassPath(LONG_CHAIN)); - assertFalse(UnsolvedSymbolVisitor.isAClassPath("TextIO.write()")); - assertFalse(UnsolvedSymbolVisitor.isAClassPath("TextIO.writeCustomType()")); - assertFalse(UnsolvedSymbolVisitor.isAClassPath(ENDS_WITH_DOT_CLASS)); - assertFalse(UnsolvedSymbolVisitor.isAClassPath(ANOTHER_LONG_CHAIN)); - assertFalse(UnsolvedSymbolVisitor.isAClassPath(ANOTHER_LONG_CHAIN_2)); + assertTrue(JavaParserUtil.isAClassPath("org.checkerframework.javacutil.ElementUtils")); + + assertFalse(JavaParserUtil.isAClassPath("org")); + assertFalse(JavaParserUtil.isAClassPath("ElementUtils")); + assertFalse(JavaParserUtil.isAClassPath("ElementUtils.foo()")); + assertFalse(JavaParserUtil.isAClassPath("org.ElementUtils.foo()")); + assertFalse(JavaParserUtil.isAClassPath("MapElements.into(TypeDescriptors.strings())")); + assertFalse(JavaParserUtil.isAClassPath(LONG_CHAIN)); + assertFalse(JavaParserUtil.isAClassPath("TextIO.write()")); + assertFalse(JavaParserUtil.isAClassPath("TextIO.writeCustomType()")); + assertFalse(JavaParserUtil.isAClassPath(ENDS_WITH_DOT_CLASS)); + assertFalse(JavaParserUtil.isAClassPath(ANOTHER_LONG_CHAIN)); + assertFalse(JavaParserUtil.isAClassPath(ANOTHER_LONG_CHAIN_2)); // This is the one that caused https://github.com/kelloggm/specimin/issues/94 - assertFalse(UnsolvedSymbolVisitor.isAClassPath(ANOTHER_LONG_CHAIN_3)); + assertFalse(JavaParserUtil.isAClassPath(ANOTHER_LONG_CHAIN_3)); } } diff --git a/src/test/java/org/checkerframework/specimin/UnusedImportsTest.java b/src/test/java/org/checkerframework/specimin/UnusedImportsTest.java new file mode 100644 index 000000000..43ea549cb --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/UnusedImportsTest.java @@ -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 imports. */ +public class UnusedImportsTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "unusedimports", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#shouldNotBeRemoved()"}); + } +} diff --git a/src/test/resources/abstractimpl/expected/com/example/Simple.java b/src/test/resources/abstractimpl/expected/com/example/Simple.java index 01ad48680..675a0cc5f 100644 --- a/src/test/resources/abstractimpl/expected/com/example/Simple.java +++ b/src/test/resources/abstractimpl/expected/com/example/Simple.java @@ -3,8 +3,6 @@ import java.util.Set; import java.util.Collection; -import com.example.WrappedSet; - public class Simple { Collection bar(K key, Collection collection) { return new WrappedSet(key, (Set) collection); diff --git a/src/test/resources/issue103/expected/com/example/AccessOrderDeque.java b/src/test/resources/issue103/expected/com/example/AccessOrderDeque.java index 3c351b1d7..8a0b1621a 100644 --- a/src/test/resources/issue103/expected/com/example/AccessOrderDeque.java +++ b/src/test/resources/issue103/expected/com/example/AccessOrderDeque.java @@ -1,6 +1,4 @@ package com.example; -import java.util.Deque; - public final class AccessOrderDeque extends AbstractLinkedDeque { } diff --git a/src/test/resources/preserveannotations/expected/com/example/Foo.java b/src/test/resources/preserveannotations/expected/com/example/Foo.java index a53376cce..0e8e2ab8d 100644 --- a/src/test/resources/preserveannotations/expected/com/example/Foo.java +++ b/src/test/resources/preserveannotations/expected/com/example/Foo.java @@ -1,6 +1,5 @@ package com.example; -import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.Positive; class Foo { diff --git a/src/test/resources/unsolvedmethodtypecorrect/expected/com/example/Simple.java b/src/test/resources/unsolvedmethodtypecorrect/expected/com/example/Simple.java index 12bc151ac..6cc22dbda 100644 --- a/src/test/resources/unsolvedmethodtypecorrect/expected/com/example/Simple.java +++ b/src/test/resources/unsolvedmethodtypecorrect/expected/com/example/Simple.java @@ -1,6 +1,5 @@ package com.example; -import org.example.LocalVariables; import org.example.Foo; class Simple { diff --git a/src/test/resources/unsolvedstaticmethod/expected/com/example/Simple.java b/src/test/resources/unsolvedstaticmethod/expected/com/example/Simple.java index 886f874ff..509ead013 100644 --- a/src/test/resources/unsolvedstaticmethod/expected/com/example/Simple.java +++ b/src/test/resources/unsolvedstaticmethod/expected/com/example/Simple.java @@ -1,6 +1,5 @@ package com.example; -import com.example.MyClass; import unreal.pack.AClass; class Simple { diff --git a/src/test/resources/unusedimports/expected/com/example/ManyImports.java b/src/test/resources/unusedimports/expected/com/example/ManyImports.java new file mode 100644 index 000000000..9aa3a03aa --- /dev/null +++ b/src/test/resources/unusedimports/expected/com/example/ManyImports.java @@ -0,0 +1,4 @@ +package com.example; + +public class ManyImports { +} diff --git a/src/test/resources/unusedimports/expected/com/example/Simple.java b/src/test/resources/unusedimports/expected/com/example/Simple.java new file mode 100644 index 000000000..c0b859517 --- /dev/null +++ b/src/test/resources/unusedimports/expected/com/example/Simple.java @@ -0,0 +1,17 @@ +package com.example; + +import java.util.List; +import java.io.*; +import static java.util.Map.*; +import static java.lang.Math.sqrt; +import static java.lang.Math.PI; + +public class Simple { + + public ManyImports shouldNotBeRemoved() { + List> x; + sqrt(PI); + File file; + return null; + } +} diff --git a/src/test/resources/unusedimports/input/com/example/ManyImports.java b/src/test/resources/unusedimports/input/com/example/ManyImports.java new file mode 100644 index 000000000..b28d59d75 --- /dev/null +++ b/src/test/resources/unusedimports/input/com/example/ManyImports.java @@ -0,0 +1,15 @@ +package com.example; + +import java.util.List; +import java.io.*; +import static java.util.Map.*; +import static java.lang.Math.sqrt; +import static java.lang.Math.PI; + +public class ManyImports { + public void shouldBeRemoved() { + List> x; + sqrt(PI); + File file; + } +} diff --git a/src/test/resources/unusedimports/input/com/example/Simple.java b/src/test/resources/unusedimports/input/com/example/Simple.java new file mode 100644 index 000000000..7c47315f1 --- /dev/null +++ b/src/test/resources/unusedimports/input/com/example/Simple.java @@ -0,0 +1,16 @@ +package com.example; + +import java.util.List; +import java.io.*; +import static java.util.Map.*; +import static java.lang.Math.sqrt; +import static java.lang.Math.PI; + +public class Simple { + public ManyImports shouldNotBeRemoved() { + List> x; + sqrt(PI); + File file; + return null; + } +} diff --git a/src/test/resources/voidreturndouble/expected/com/example/MyFoo.java b/src/test/resources/voidreturndouble/expected/com/example/MyFoo.java index fb5aee674..805150d0b 100644 --- a/src/test/resources/voidreturndouble/expected/com/example/MyFoo.java +++ b/src/test/resources/voidreturndouble/expected/com/example/MyFoo.java @@ -1,7 +1,6 @@ package com.example; import org.example.Foo; -import org.example.MethodGen; public class MyFoo extends Foo { diff --git a/src/test/resources/whenthen/expected/com/example/Simple.java b/src/test/resources/whenthen/expected/com/example/Simple.java index 4c79c6afc..770dc4784 100644 --- a/src/test/resources/whenthen/expected/com/example/Simple.java +++ b/src/test/resources/whenthen/expected/com/example/Simple.java @@ -3,8 +3,6 @@ import static org.example.TestUtil.when; import static org.example.TestUtil.mock; -import com.example.Banana; - class Simple { void bar() {