diff --git a/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java index b13a6cc1e..6f08f348e 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java @@ -1,6 +1,7 @@ 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.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.EnumConstantDeclaration; @@ -8,8 +9,8 @@ import com.github.javaparser.ast.body.FieldDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; -import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.AssignExpr; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.FieldAccessExpr; import com.github.javaparser.ast.expr.LambdaExpr; @@ -24,17 +25,19 @@ import com.github.javaparser.ast.type.ReferenceType; import com.github.javaparser.ast.type.Type; import com.github.javaparser.ast.type.UnionType; -import com.github.javaparser.ast.visitor.ModifierVisitor; import com.github.javaparser.ast.visitor.Visitable; +import com.github.javaparser.resolution.MethodUsage; import com.github.javaparser.resolution.UnsolvedSymbolException; import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration; +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.ResolvedParameterDeclaration; import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration; import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; import com.github.javaparser.resolution.types.ResolvedReferenceType; import com.github.javaparser.resolution.types.ResolvedType; -import com.google.common.base.Splitter; +import com.github.javaparser.resolution.types.ResolvedWildcard; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -42,45 +45,22 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; + /** - * The main visitor for Specimin's first phase, which locates the target method(s) and compiles + * The main visitor for Specimin's first phase, which locates the target member(s) and compiles * information on what specifications they use. */ -public class TargetMethodFinderVisitor extends ModifierVisitor { +public class TargetMemberFinderVisitor extends SpeciminStateVisitor { /** * The names of the target methods. The format is * class.fully.qualified.Name#methodName(Param1Type, Param2Type, ...). All the names will have * spaces remove for ease of comparison. */ - private Set targetMethodNames; - /** The names of the target fields. The format is class.fully.qualified.Name#fieldName. */ - private Set targetFieldNames; - /** - * This boolean tracks whether the element currently being visited is inside a target method. It - * is set by {@link #visit(MethodDeclaration, Void)}. - */ - private boolean insideTargetMember = false; - /** The fully-qualified name of the class currently being visited. */ - private String classFQName = ""; - /** - * The members (methods and fields) that were actually used by the targets, and therefore ought to - * have their specifications (but not bodies) preserved. The Strings in the set are the - * fully-qualified names, as returned by ResolvedMethodDeclaration#getQualifiedSignature for - * methods and FieldAccessExpr#getName for fields. - */ - private final Set usedMembers = new HashSet<>(); - /** - * Type elements (classes, interfaces, and enums) related to the methods used by the targets. - * These classes will be included in the input. - */ - private Set usedTypeElement = new HashSet<>(); - /** Set of variables declared in this current class */ - private final Set declaredNames = new HashSet<>(); - /** - * The resolved target methods. The Strings in the set are the fully-qualified names, as returned - * by ResolvedMethodDeclaration#getQualifiedSignature. - */ - private final Set targetMethods = new HashSet<>(); + private final Set targetMethodNames; + + /** The name of the package currently being visited. */ + private String currentPackage = ""; + /** * The keys of this map are a local copy of the input list of methods. A method is removed from * this copy's key set when it is located. If the visitor has been run on all source files and the @@ -90,22 +70,24 @@ public class TargetMethodFinderVisitor extends ModifierVisitor { * name/signature of a method actually is. */ private final Map> unfoundMethods; - /** - * This map has the name of an imported class as key and the package of that class as the value. - */ - private final Map importedClassToPackage; + + /** Same as the unfoundMethods set, but for fields */ + private final Map> unfoundFields = new HashMap<>(); + /** * This map connects the resolved declaration of a method to the interface that contains it, if * any. */ private final Map methodDeclarationToInterfaceType = new HashMap<>(); + /** * This map connects the fully-qualified names of non-primary classes with the fully-qualified * names of their corresponding primary classes. A primary class is a class that has the same name * as the Java file where the class is declared. */ Map nonPrimaryClassesToPrimaryClass; + /** * JavaParser is not perfect. Sometimes it can't solve resolved method calls if they have * complicated type variables or if the receiver is the parameter of a lambda expression. We keep @@ -115,34 +97,27 @@ public class TargetMethodFinderVisitor extends ModifierVisitor { * appended. Anything that matches the latter will later be preserved. */ private final Set resolvedYetStuckMethodCall = new HashSet<>(); + /** * Create a new target method finding visitor. * - * @param methodNames the names of the target methods, the format - * class.fully.qualified.Name#methodName(Param1Type, Param2Type, ...) - * @param fieldNames the names of the target fields, the format is - * class.fully.qualified.Name#fieldName + * @param previous the previous Specimin visitor * @param nonPrimaryClassesToPrimaryClass map connecting non-primary classes with their * corresponding primary classes - * @param usedTypeElement set of type elements used by target methods. */ - public TargetMethodFinderVisitor( - List methodNames, - List fieldNames, - Map nonPrimaryClassesToPrimaryClass, - Set usedTypeElement) { + public TargetMemberFinderVisitor( + SpeciminStateVisitor previous, Map nonPrimaryClassesToPrimaryClass) { + super(previous); targetMethodNames = new HashSet<>(); - for (String methodSignature : methodNames) { + for (String methodSignature : targetMethods) { this.targetMethodNames.add(methodSignature.replaceAll("\\s", "")); } - targetFieldNames = new HashSet<>(); - targetFieldNames.addAll(fieldNames); - unfoundMethods = new HashMap<>(methodNames.size()); + unfoundMethods = new HashMap<>(targetMethods.size()); targetMethodNames.forEach(m -> unfoundMethods.put(m, new HashSet<>())); - importedClassToPackage = new HashMap<>(); + targetFields.forEach(f -> unfoundFields.put(f, new HashSet<>())); this.nonPrimaryClassesToPrimaryClass = nonPrimaryClassesToPrimaryClass; - this.usedTypeElement = usedTypeElement; } + /** * Returns the methods that so far this visitor has not located from its target list. Usually, * this should be checked after running the visitor to ensure that it is empty. The targets are @@ -155,34 +130,20 @@ public TargetMethodFinderVisitor( public Map> getUnfoundMethods() { return unfoundMethods; } + /** - * Get the methods that this visitor has concluded that the target method(s) use, and therefore - * ought to be retained. The Strings in the set are the fully-qualified names, as returned by - * ResolvedMethodDeclaration#getQualifiedSignature. - * - * @return the used methods - */ - public Set getUsedMembers() { - return usedMembers; - } - /** - * Get the classes of the methods and enums that the target method uses. The Strings in the set - * are the fully-qualified names. - * - * @return the used type elements. - */ - public Set getUsedTypeElement() { - return usedTypeElement; - } - /** - * Get the target methods that this visitor has encountered so far. The Strings in the set are the - * fully-qualified names, as returned by ResolvedMethodDeclaration#getQualifiedSignature. + * Returns the fields that so far this visitor has not located from its target list. Usually, this + * should be checked after running the visitor to ensure that it is empty. The targets are the + * keys in the returned maps; the values are fields in the same class that were considered but + * were not the target (useful for issuing error messages). * - * @return the target methods + * @return the fields that so far this visitor has not located from its target list, mapped to the + * candidate fields that were considered */ - public Set getTargetMethods() { - return targetMethods; + public Map> getUnfoundFields() { + return unfoundFields; } + /** * Get the set of resolved yet stuck method calls. * @@ -191,6 +152,7 @@ public Set getTargetMethods() { public Set getResolvedYetStuckMethodCall() { return resolvedYetStuckMethodCall; } + /** * 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. @@ -204,6 +166,7 @@ private void updateMethodDeclarationToInterfaceType( this.methodDeclarationToInterfaceType.put(method, interfaceType); } } + /** * Updates unfoundMethods so that the appropriate elements have their set of considered methods * updated to match a method that was not a target method. @@ -213,8 +176,9 @@ private void updateMethodDeclarationToInterfaceType( private void updateUnfoundMethods(String methodAsString) { Set targetMethodsInClass = targetMethodNames.stream() - .filter(t -> t.startsWith(this.classFQName)) + .filter(t -> t.startsWith(this.currentClassQualifiedName)) .collect(Collectors.toSet()); + for (String targetMethodInClass : targetMethodsInClass) { // This check is necessary to avoid an NPE if the target method // in question has already been removed from unfoundMethods. @@ -223,124 +187,83 @@ private void updateUnfoundMethods(String methodAsString) { } } } + + /** + * Updates unfoundFields so that the appropriate elements have their set of considered fields + * updated to match a field that was not a target field. + * + * @param fieldAsString the field that wasn't a target field + */ + private void updateUnfoundFields(String fieldAsString) { + Set targetFieldsInClass = + targetFields.stream() + .filter(t -> t.startsWith(this.currentClassQualifiedName)) + .collect(Collectors.toSet()); + + for (String targetFieldInClass : targetFieldsInClass) { + // This check is necessary to avoid an NPE if the target field + // in question has already been removed from unfoundFields. + if (unfoundFields.containsKey(targetFieldInClass)) { + unfoundFields.get(targetFieldInClass).add(fieldAsString); + } + } + } + @Override - public Node visit(ImportDeclaration decl, Void p) { - String classFullName = decl.getNameAsString(); - if (decl.isStatic()) { - classFullName = classFullName.substring(0, classFullName.lastIndexOf(".")); - } - String classSimpleName = classFullName.substring(classFullName.lastIndexOf(".") + 1); - String packageName = classFullName.replace("." + classSimpleName, ""); - importedClassToPackage.put(classSimpleName, packageName); + public Visitable visit(PackageDeclaration decl, Void p) { + this.currentPackage = decl.getNameAsString(); return super.visit(decl, p); } + @Override public Visitable visit(ClassOrInterfaceDeclaration decl, Void p) { for (ClassOrInterfaceType interfaceType : decl.getImplementedTypes()) { try { updateMethodDeclarationToInterfaceType( - interfaceType.resolve().getAllMethods(), interfaceType); + JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(interfaceType) + .getAllMethods(), + interfaceType); } catch (UnsolvedSymbolException e) { continue; } } - manageClassFQNamePreSuper(decl); - Visitable result = super.visit(decl, p); - manageClassFQNamePostSuper(decl); - return result; - } - @Override - public Visitable visit(EnumDeclaration decl, Void p) { - manageClassFQNamePreSuper(decl); - Visitable result = super.visit(decl, p); - manageClassFQNamePostSuper(decl); - return result; - } - /** - * Helper method to share code for managing the {@link #classFQName} field between - * classes/interfaces and enums. Call this method before calling super.visit(). - * - * @see #classFQName - * @see #manageClassFQNamePostSuper(TypeDeclaration) - * @param decl a class, interface, or enum declaration - */ - private void manageClassFQNamePreSuper(TypeDeclaration decl) { - if (decl.isNestedType()) { - this.classFQName += "." + decl.getName().toString(); - } else { - boolean isLocalClass = - decl.isClassOrInterfaceDeclaration() - && decl.asClassOrInterfaceDeclaration().isLocalClassDeclaration(); - if (!isLocalClass) { - if (!this.classFQName.equals("")) { - throw new UnsupportedOperationException( - "Attempted to enter an unexpected kind of class: " - - - - - - - - Expand All - - @@ -323,7 +320,7 @@ private void manageClassFQNamePreSuper(TypeDeclaration decl) { - - + decl.getFullyQualifiedName() - + " but already had a set classFQName: " - + classFQName); - } - // Should always be present. - this.classFQName = decl.getFullyQualifiedName().orElseThrow(); - } - } - } - /** - * Helper method to share code for managing the {@link #classFQName} field between - * classes/interfaces and enums. Call this method after calling super.visit(). - * - * @see #classFQName - * @see #manageClassFQNamePreSuper(TypeDeclaration) - * @param decl a class, interface, or enum declaration - */ - private void manageClassFQNamePostSuper(TypeDeclaration decl) { - if (decl.isNestedType()) { - this.classFQName = this.classFQName.substring(0, this.classFQName.lastIndexOf('.')); - } else { - this.classFQName = ""; - } - } - - - - + return super.visit(decl, p); + } - - Expand Down - - - @Override public Visitable visit(ConstructorDeclaration method, Void p) { String constructorMethodAsString = method.getDeclarationAsString(false, false, false); // the methodName will be something like this: "com.example.Car#Car()" - String methodName = this.classFQName + "#" + constructorMethodAsString; - boolean oldInsideTargetMember = insideTargetMember; + String methodName = this.currentClassQualifiedName + "#" + constructorMethodAsString; + // remove spaces + methodName = methodName.replaceAll("\\s", ""); if (this.targetMethodNames.contains(methodName)) { - insideTargetMember = true; ResolvedConstructorDeclaration resolvedMethod = method.resolve(); targetMethods.add(resolvedMethod.getQualifiedSignature()); unfoundMethods.remove(methodName); updateUsedClassWithQualifiedClassName( resolvedMethod.getPackageName() + "." + resolvedMethod.getClassName(), - usedTypeElement, + usedTypeElements, nonPrimaryClassesToPrimaryClass); + if (modularityModel.preserveAllFieldsIfTargetIsConstructor()) { + // This cast is safe, because a constructor must be contained in a class declaration. + ClassOrInterfaceDeclaration thisClass = + (ClassOrInterfaceDeclaration) JavaParserUtil.getEnclosingClassLike(method); + for (FieldDeclaration field : thisClass.getFields()) { + for (VariableDeclarator variable : field.getVariables()) { + usedMembers.add(currentClassQualifiedName + "#" + variable.getNameAsString()); + ResolvedType fieldType = variable.resolve().getType(); + updateUsedClassBasedOnType(fieldType); + } + } + } } else { updateUnfoundMethods(methodName); } + Visitable result = super.visit(method, p); - insideTargetMember = oldInsideTargetMember; + if (method.getParentNode().isEmpty()) { return result; } @@ -350,7 +273,7 @@ public Visitable visit(ConstructorDeclaration method, Void p) { return result; } // used enums needs to have compilable constructors. - if (usedTypeElement.contains(parentNode.getFullyQualifiedName().get())) { + if (usedTypeElements.contains(parentNode.getFullyQualifiedName().orElseThrow())) { for (Parameter parameter : method.getParameters()) { updateUsedClassBasedOnType(parameter.getType().resolve()); } @@ -358,29 +281,59 @@ public Visitable visit(ConstructorDeclaration method, Void p) { } return result; } + @Override public Visitable visit(VariableDeclarator node, Void arg) { - declaredNames.add(node.getNameAsString()); if (node.getParentNode().isPresent() && node.getParentNode().get() instanceof FieldDeclaration) { - if (targetFieldNames.contains(this.classFQName + "#" + node.getNameAsString())) { - boolean oldInsideTargetMember = insideTargetMember; - insideTargetMember = true; - Visitable result = super.visit(node, arg); - usedTypeElement.add(this.classFQName); - insideTargetMember = oldInsideTargetMember; - return result; + String fieldName = this.currentClassQualifiedName + "#" + node.getNameAsString(); + if (targetFields.contains(fieldName)) { + ResolvedFieldDeclaration resolvedField = + ((FieldDeclaration) node.getParentNode().get()).resolve(); + unfoundFields.remove(fieldName); + updateUsedClassWithQualifiedClassName( + resolvedField.declaringType().getQualifiedName(), + usedTypeElements, + nonPrimaryClassesToPrimaryClass); + } else { + updateUnfoundFields(fieldName); } } return super.visit(node, arg); } + + @Override + public Visitable visit(AssignExpr node, Void p) { + if (insideTargetCtor) { + // check if the LHS is a field + Expression lhs = node.getTarget(); + if (lhs.isFieldAccessExpr()) { + FieldAccessExpr asFieldAccess = lhs.asFieldAccessExpr(); + Expression scope = asFieldAccess.getScope(); + if (scope.toString().equals("this")) { + fieldsAssignedByTargetCtors.add( + currentClassQualifiedName + "#" + asFieldAccess.getNameAsString()); + } + } else if (lhs.isNameExpr()) { + // could be a field of "this" + NameExpr asName = lhs.asNameExpr(); + ResolvedValueDeclaration resolved = asName.resolve(); + if (resolved.isField()) { + fieldsAssignedByTargetCtors.add( + currentClassQualifiedName + "#" + asName.getNameAsString()); + } + } + } + return super.visit(node, p); + } + @Override public Visitable visit(MethodDeclaration method, Void p) { boolean oldInsideTargetMember = insideTargetMember; - String methodDeclAsString = method.getDeclarationAsString(false, false, false); // TODO: test this with annotations - String methodWithoutReturnAndAnnos = removeMethodReturnTypeAndAnnotations(methodDeclAsString); - String methodName = this.classFQName + "#" + methodWithoutReturnAndAnnos; + String methodWithoutReturnAndAnnos = + JavaParserUtil.removeMethodReturnTypeAndAnnotations(method); + String methodName = this.currentClassQualifiedName + "#" + methodWithoutReturnAndAnnos; // this method belongs to an anonymous class inside the target method if (insideTargetMember) { Node parentNode = method.getParentNode().get(); @@ -392,7 +345,7 @@ public Visitable visit(MethodDeclaration method, Void p) { String methodClass = resolved.getClassName(); usedMembers.add(methodPackage + "." + methodClass + "." + method.getNameAsString() + "()"); updateUsedClassWithQualifiedClassName( - methodPackage + "." + methodClass, usedTypeElement, nonPrimaryClassesToPrimaryClass); + methodPackage + "." + methodClass, usedTypeElements, nonPrimaryClassesToPrimaryClass); } } String methodWithoutAnySpace = methodName.replaceAll("\\s", ""); @@ -401,8 +354,9 @@ public Visitable visit(MethodDeclaration method, Void p) { updateUsedClassesForInterface(resolvedMethod); updateUsedClassWithQualifiedClassName( resolvedMethod.getPackageName() + "." + resolvedMethod.getClassName(), - usedTypeElement, + usedTypeElements, nonPrimaryClassesToPrimaryClass); + insideTargetMember = true; targetMethods.add(resolvedMethod.getQualifiedSignature()); // make sure that differences in spacing does not interfere with the result @@ -425,14 +379,23 @@ public Visitable visit(MethodDeclaration method, Void p) { // the type variable must have been declared in one of the containing scopes, // and UnsolvedSymbolVisitor should already guarantee that the variable will // be included in one of the classes that Specimin outputs. + } catch (UnsolvedSymbolException e) { + throw new RuntimeException( + "failed to solve the return type (" + + returnType + + ") of " + + methodWithoutReturnAndAnnos, + e); } } else { updateUnfoundMethods(methodName); } + Visitable result = super.visit(method, p); insideTargetMember = oldInsideTargetMember; return result; } + @Override public Visitable visit(Parameter para, Void p) { if (insideTargetMember) { @@ -458,11 +421,12 @@ public Visitable visit(Parameter para, Void p) { throw new RuntimeException("cannot solve: " + para, e); } } + if (paramType.isReferenceType()) { String paraTypeFullName = - paramType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); + paramType.asReferenceType().getTypeDeclaration().orElseThrow().getQualifiedName(); updateUsedClassWithQualifiedClassName( - paraTypeFullName, usedTypeElement, nonPrimaryClassesToPrimaryClass); + paraTypeFullName, usedTypeElements, nonPrimaryClassesToPrimaryClass); for (ResolvedType typeParameterValue : paramType.asReferenceType().typeParametersValues()) { String typeParameterValueName = typeParameterValue.describe(); @@ -472,13 +436,15 @@ public Visitable visit(Parameter para, Void p) { typeParameterValueName.substring(0, typeParameterValueName.indexOf("<")); } updateUsedClassWithQualifiedClassName( - typeParameterValueName, usedTypeElement, nonPrimaryClassesToPrimaryClass); + typeParameterValueName, usedTypeElements, nonPrimaryClassesToPrimaryClass); } } } } + return super.visit(para, p); } + /** * Returns true iff we can prove that the parameter is a lambda parameter. This method should only * be called on parameters that are not of unknown type (which are definitely lambda params). @@ -489,6 +455,7 @@ public Visitable visit(Parameter para, Void p) { private boolean isLambdaParam(Parameter para) { return para.getParentNode().orElseThrow() instanceof LambdaExpr; } + @Override public Visitable visit(MethodReferenceExpr ref, Void p) { if (insideTargetMember) { @@ -497,6 +464,7 @@ public Visitable visit(MethodReferenceExpr ref, Void p) { } return super.visit(ref, p); } + @Override public Visitable visit(MethodCallExpr call, Void p) { if (insideTargetMember) { @@ -521,7 +489,45 @@ public Visitable visit(MethodCallExpr call, Void p) { // leading to a RuntimeException. // Note: this preservation is safe because we are not having an UnsolvedSymbolException. // Only unsolved symbols can make the output failed to compile. - resolvedYetStuckMethodCall.add(this.classFQName + "." + call.getNameAsString()); + if (call.hasScope()) { + Expression scope = call.getScope().orElseThrow(); + String scopeAsString = scope.toString(); + if (scopeAsString.equals("this") || scopeAsString.equals("super")) { + // In the "super" case, it would be better to add the name of an + // extended or implemented class/interface. However, there are two complications: + // 1) we currently don't track the list of classes/interfaces that the current class + // extends and/or implements in this visitor and 2) even if we did track that, there + // is no way for us to know which of those classes/interfaces the method belongs to. + // TODO: write a test for the "super" case and then figure out a better way to handle + // it. + resolvedYetStuckMethodCall.add( + this.currentClassQualifiedName + "." + call.getNameAsString()); + } else { + // Use the scope instead. First, check if it's resolvable. If it is, great - + // just use that. If not, then we need to use some heuristics as fallbacks. + try { + ResolvedType scopeType = scope.calculateResolvedType(); + resolvedYetStuckMethodCall.add(scopeType.describe() + "." + call.getNameAsString()); + usedTypeElements.add(scopeType.describe()); + } catch (Exception e1) { + // There are two fallback cases: the scope is an FQN (e.g., in + // 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)) { + resolvedYetStuckMethodCall.add(scopeAsString + "." + call.getNameAsString()); + usedTypeElements.add(scopeAsString); + } else { + resolvedYetStuckMethodCall.add( + getCurrentPackage() + "." + scopeAsString + "." + call.getNameAsString()); + usedTypeElements.add(getCurrentPackage() + "." + scopeAsString); + } + } + } + } else { + resolvedYetStuckMethodCall.add( + this.currentClassQualifiedName + "." + call.getNameAsString()); + } return super.visit(call, p); } preserveMethodDecl(decl); @@ -531,11 +537,24 @@ public Visitable visit(MethodCallExpr call, Void p) { Expression arg = call.getArgument(i); if (arg.isLambdaExpr()) { updateUsedClassBasedOnType(decl.getParam(i).getType()); + // We should mark the abstract method for preservation as well + if (decl.getParam(i).getType().isReferenceType()) { + ResolvedReferenceType functionalInterface = + decl.getParam(i).getType().asReferenceType(); + for (MethodUsage method : functionalInterface.getDeclaredMethods()) { + if (method.getDeclaration().isAbstract()) { + preserveMethodDecl(method.getDeclaration()); + // Only one abstract method per functional interface + break; + } + } + } } } } return super.visit(call, p); } + /** * Helper method for preserving a used method. This code is called for both method call * expressions and method refs. @@ -546,7 +565,7 @@ private void preserveMethodDecl(ResolvedMethodDeclaration decl) { usedMembers.add(decl.getQualifiedSignature()); updateUsedClassWithQualifiedClassName( decl.getPackageName() + "." + decl.getClassName(), - usedTypeElement, + usedTypeElements, nonPrimaryClassesToPrimaryClass); try { ResolvedType methodReturnType = decl.getReturnType(); @@ -560,14 +579,32 @@ private void preserveMethodDecl(ResolvedMethodDeclaration decl) { catch (UnsolvedSymbolException e) { return; } + + for (int i = 0; i < decl.getNumberOfParams(); ++i) { + // Why is there no getParams() method?? + ResolvedParameterDeclaration p = decl.getParam(i); + ResolvedType pType = p.getType(); + updateUsedClassBasedOnType(pType); + } } + + /** + * Gets the package name of the current class. + * + * @return the current package name + */ + private String getCurrentPackage() { + return currentPackage; + } + @Override public Visitable visit(ClassOrInterfaceType type, Void p) { if (!insideTargetMember) { return super.visit(type, p); } try { - ResolvedReferenceType typeResolved = type.resolve(); + ResolvedReferenceType typeResolved = + JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(type); updateUsedClassBasedOnType(typeResolved); } // if the type has a fully-qualified form, JavaParser also consider other components rather than @@ -579,18 +616,30 @@ public Visitable visit(ClassOrInterfaceType type, Void p) { } return super.visit(type, p); } + @Override public Visitable visit(ObjectCreationExpr newExpr, Void p) { if (insideTargetMember) { - ResolvedConstructorDeclaration resolved = newExpr.resolve(); - usedMembers.add(resolved.getQualifiedSignature()); - updateUsedClassWithQualifiedClassName( - resolved.getPackageName() + "." + resolved.getClassName(), - usedTypeElement, - nonPrimaryClassesToPrimaryClass); + try { + ResolvedConstructorDeclaration resolved = newExpr.resolve(); + usedMembers.add(resolved.getQualifiedSignature()); + updateUsedClassWithQualifiedClassName( + resolved.getPackageName() + "." + resolved.getClassName(), + usedTypeElements, + nonPrimaryClassesToPrimaryClass); + for (int i = 0; i < resolved.getNumberOfParams(); ++i) { + // Why is there no getParams() method?? + ResolvedParameterDeclaration param = resolved.getParam(i); + ResolvedType pType = param.getType(); + updateUsedClassBasedOnType(pType); + } + } catch (UnsolvedSymbolException e) { + throw new RuntimeException("trying to resolve : " + newExpr, e); + } } return super.visit(newExpr, p); } + @Override public Visitable visit(ExplicitConstructorInvocationStmt expr, Void p) { if (insideTargetMember) { @@ -598,28 +647,35 @@ public Visitable visit(ExplicitConstructorInvocationStmt expr, Void p) { usedMembers.add(resolved.getQualifiedSignature()); updateUsedClassWithQualifiedClassName( resolved.getPackageName() + "." + resolved.getClassName(), - usedTypeElement, + usedTypeElements, nonPrimaryClassesToPrimaryClass); } return super.visit(expr, p); } + @Override public Visitable visit(EnumConstantDeclaration enumConstantDeclaration, Void p) { - Node parentNode = enumConstantDeclaration.getParentNode().get(); + Node parentNode = enumConstantDeclaration.getParentNode().orElseThrow(); + if (parentNode instanceof EnumDeclaration) { - if (usedTypeElement.contains( - ((EnumDeclaration) parentNode).asEnumDeclaration().getFullyQualifiedName().get())) { + if (usedTypeElements.contains( + ((EnumDeclaration) parentNode) + .asEnumDeclaration() + .getFullyQualifiedName() + .orElseThrow())) { boolean oldInsideTargetMember = insideTargetMember; // used enum constant are not strictly target methods, but we need to make sure the symbols // inside them are preserved. insideTargetMember = true; Visitable result = super.visit(enumConstantDeclaration, p); insideTargetMember = oldInsideTargetMember; + return result; } } return super.visit(enumConstantDeclaration, p); } + @Override public Visitable visit(FieldAccessExpr expr, Void p) { if (insideTargetMember) { @@ -633,15 +689,21 @@ public Visitable visit(FieldAccessExpr expr, Void p) { fullNameOfClass = expr.resolve().asField().declaringType().getQualifiedName(); usedMembers.add(fullNameOfClass + "#" + expr.getName().asString()); updateUsedClassWithQualifiedClassName( - fullNameOfClass, usedTypeElement, nonPrimaryClassesToPrimaryClass); + fullNameOfClass, usedTypeElements, nonPrimaryClassesToPrimaryClass); ResolvedType exprResolvedType = expr.resolve().getType(); updateUsedClassBasedOnType(exprResolvedType); } catch (UnsolvedSymbolException | UnsupportedOperationException e) { // when the type is a primitive array, we will have an UnsupportedOperationException if (e instanceof UnsupportedOperationException) { - updateUsedElementWithPotentialFieldNameExpr(expr.getScope().asNameExpr()); + Expression scope = expr.getScope(); + if (scope.isNameExpr()) { + updateUsedElementWithPotentialFieldNameExpr(scope.asNameExpr()); + } + // If the scope is not a name expression, then it must be "this" (handled elsewhere), + // "super" (handled directly below), or another field access expression (handled by + // the visitor), so there's nothing to do. } - // if the a field is accessed in the form of a fully-qualified path, such as + // if 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. } @@ -653,6 +715,7 @@ public Visitable visit(FieldAccessExpr expr, Void p) { } return super.visit(expr, p); } + @Override public Visitable visit(NameExpr expr, Void p) { if (insideTargetMember) { @@ -663,6 +726,7 @@ 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 @@ -680,12 +744,12 @@ public void updateUsedClassesForInterface(ResolvedMethodDeclaration method) { .describe() .equals(interfaceMethod.getReturnType().describe())) { if (method.getNumberOfParams() == interfaceMethod.getNumberOfParams()) { + ResolvedReferenceType resolvedInterface = + JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType( + methodDeclarationToInterfaceType.get(interfaceMethod)); updateUsedClassWithQualifiedClassName( - methodDeclarationToInterfaceType - .get(interfaceMethod) - .resolve() - .getQualifiedName(), - usedTypeElement, + resolvedInterface.getQualifiedName(), + usedTypeElements, nonPrimaryClassesToPrimaryClass); usedMembers.add(interfaceMethod.getQualifiedSignature()); } @@ -697,29 +761,7 @@ public void updateUsedClassesForInterface(ResolvedMethodDeclaration method) { } } } - /** - * Given a method declaration, this method return the declaration of that method without the - * return type and any possible annotation. - * - * @param methodDeclaration the method declaration to be used as input - * @return methodDeclaration without the return type and any possible annotation. - */ - public static String removeMethodReturnTypeAndAnnotations(String methodDeclaration) { - String methodDeclarationWithoutParen = - methodDeclaration.substring(0, methodDeclaration.indexOf("(")); - List methodParts = Splitter.onPattern(" ").splitToList(methodDeclarationWithoutParen); - String methodName = methodParts.get(methodParts.size() - 1); - String methodReturnType = methodDeclaration.substring(0, methodDeclaration.indexOf(methodName)); - String methodWithoutReturnType = methodDeclaration.replace(methodReturnType, ""); - methodParts = Splitter.onPattern(" ").splitToList(methodWithoutReturnType); - String filteredMethodDeclaration = - methodParts.stream() - .filter(part -> !part.startsWith("@")) - .map(part -> part.indexOf('@') == -1 ? part : part.substring(0, part.indexOf('@'))) - .collect(Collectors.joining(" ")); - // sometimes an extra space may occur if an annotation right after a < was removed - return filteredMethodDeclaration.replace("< ", "<"); - } + /** * Resolves unionType parameters one by one and adds them in the usedClass set. * @@ -731,6 +773,7 @@ private void resolveUnionType(UnionType type) { updateUsedClassBasedOnType(paramType); } } + /** * Given a FieldAccessExpr, this method updates the sets of used classes and members if this field * is actually an enum constant. @@ -754,10 +797,11 @@ private boolean updateUsedClassAndMemberForEnumConstant(FieldAccessExpr fieldAcc } String classFullName = resolved.asEnumConstant().getType().describe(); updateUsedClassWithQualifiedClassName( - classFullName, usedTypeElement, nonPrimaryClassesToPrimaryClass); + classFullName, usedTypeElements, nonPrimaryClassesToPrimaryClass); usedMembers.add(classFullName + "." + fieldAccessExpr.getNameAsString()); return true; } + /** * Given a NameExpr instance, this method will update the used elements, classes and members if * that NameExpr is a field. @@ -777,11 +821,19 @@ public void updateUsedElementWithPotentialFieldNameExpr(NameExpr expr) { // field is declared String classFullName = exprDecl.asField().declaringType().getQualifiedName(); updateUsedClassWithQualifiedClassName( - classFullName, usedTypeElement, nonPrimaryClassesToPrimaryClass); + classFullName, usedTypeElements, nonPrimaryClassesToPrimaryClass); usedMembers.add(classFullName + "#" + expr.getNameAsString()); updateUsedClassBasedOnType(exprDecl.getType()); + } else if (exprDecl instanceof ResolvedEnumConstantDeclaration) { + String enumFullName = exprDecl.asEnumConstant().getType().describe(); + updateUsedClassWithQualifiedClassName( + enumFullName, usedTypeElements, nonPrimaryClassesToPrimaryClass); + // "." and not "#" because enum constants are not fields + usedMembers.add(enumFullName + "." + expr.getNameAsString()); + updateUsedClassBasedOnType(exprDecl.getType()); } } + /** * Updates the list of used type elements with the given qualified type name and its corresponding * primary type and enclosing type. This includes cases such as classes not sharing the same name @@ -797,6 +849,7 @@ public static void updateUsedClassWithQualifiedClassName( String qualifiedClassName, Set usedTypeElement, Map nonPrimaryClassesToPrimaryClass) { + // in case of type variables if (!qualifiedClassName.contains(".")) { return; @@ -813,6 +866,7 @@ public static void updateUsedClassWithQualifiedClassName( usedTypeElement, nonPrimaryClassesToPrimaryClass); } + String potentialOuterClass = qualifiedClassName.substring(0, qualifiedClassName.lastIndexOf(".")); if (UnsolvedSymbolVisitor.isAClassPath(potentialOuterClass)) { @@ -820,9 +874,12 @@ public static void updateUsedClassWithQualifiedClassName( potentialOuterClass, usedTypeElement, nonPrimaryClassesToPrimaryClass); } } + /** * Updates the list of used classes based on the resolved type of a used element, where a element - * can be a method, a field, a variable, or a parameter. + * can be a method, a field, a variable, or a parameter. Also updates the set of used classes + * based on component types, wildcard bounds, etc., as needed: any type that is used in the type + * will be included. * * @param type The resolved type of the used element. */ @@ -833,24 +890,37 @@ public void updateUsedClassBasedOnType(ResolvedType type) { ResolvedTypeParameterDeclaration asTypeParameter = type.asTypeParameter(); for (ResolvedTypeParameterDeclaration.Bound bound : asTypeParameter.getBounds()) { updateUsedClassWithQualifiedClassName( - bound.getType().describe(), usedTypeElement, nonPrimaryClassesToPrimaryClass); + bound.getType().describe(), usedTypeElements, nonPrimaryClassesToPrimaryClass); } return; + } else if (type.isArray()) { + ResolvedType componentType = type.asArrayType().getComponentType(); + updateUsedClassBasedOnType(componentType); + return; } updateUsedClassWithQualifiedClassName( - type.describe(), usedTypeElement, nonPrimaryClassesToPrimaryClass); + type.describe(), usedTypeElements, nonPrimaryClassesToPrimaryClass); if (!type.isReferenceType()) { return; } ResolvedReferenceType typeAsReference = type.asReferenceType(); List typeParameters = typeAsReference.typeParametersValues(); for (ResolvedType typePara : typeParameters) { - if (typePara.isPrimitive() || typePara.isTypeVariable() || typePara.isWildcard()) { + if (typePara.isPrimitive() || typePara.isTypeVariable()) { + // Nothing to do, since these are already in-scope. + continue; + } + if (typePara.isWildcard()) { + ResolvedWildcard asWildcard = typePara.asWildcard(); + // Recurse into the bound, if one exists. + if (asWildcard.isBounded()) { + updateUsedClassBasedOnType(asWildcard.getBoundedType()); + } continue; } updateUsedClassWithQualifiedClassName( typePara.asReferenceType().getQualifiedName(), - usedTypeElement, + usedTypeElements, nonPrimaryClassesToPrimaryClass); } }