From 20332f8e74483ba54473dde011d0a59e98fc62c9 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Nov 2023 21:22:30 -0500 Subject: [PATCH 01/23] =?UTF-8?q?add=20codes=20for=20tricky=20p=C3=A1?= =?UTF-8?q?=C2=BA=C3=A1=C2=BA=C3=A1=C2=BAarameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specimin/SpeciminRunner.java | 20 +++++++++++++++-- .../specimin/TargetMethodFinderVisitor.java | 16 ++++++++++++++ .../specimin/UnsolvedClass.java | 22 ++++++++++++++++++- .../specimin/UnsolvedParameter.java | 15 +++++++++++++ .../expected/com/example/Simple.java | 12 ++++++++++ .../javaparser/ast/ImportDeclaration.java | 4 ++++ .../com/github/javaparser/ast/NodeList.java | 4 ++++ .../input/com/example/Simple.java | 13 +++++++++++ 8 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/checkerframework/specimin/UnsolvedParameter.java create mode 100644 src/test/resources/unsolvedparameter/expected/com/example/Simple.java create mode 100644 src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/ImportDeclaration.java create mode 100644 src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/NodeList.java create mode 100644 src/test/resources/unsolvedparameter/input/com/example/Simple.java diff --git a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java index a489ef42..5450d30e 100644 --- a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java +++ b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java @@ -26,6 +26,7 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; +import org.checkerframework.checker.signature.qual.ClassGetSimpleName; /** This class is the main runner for Specimin. Use its main() method to start Specimin. */ public class SpeciminRunner { @@ -165,7 +166,9 @@ public static void performMinimization( // correct the types of all related files before adding them to parsedTargetFiles JavaTypeCorrect typeCorrecter = new JavaTypeCorrect(root, relatedClass); typeCorrecter.correctTypesForAllFiles(); - addMissingClass.updateTypes(typeCorrecter.getTypeToChange()); + Map<@ClassGetSimpleName String, @ClassGetSimpleName String> typeToChange = + typeCorrecter.getTypeToChange(); + addMissingClass.updateTypes(typeToChange); for (String directory : relatedClass) { // directories already in parsedTargetFiles are original files in the root directory, we are @@ -187,7 +190,20 @@ public static void performMinimization( if (isEmptyCompilationUnit(target.getValue())) { // target key will have this form: "path/of/package/ClassName.java" String classFullyQualfiedName = target.getKey().replace("/", "."); - classFullyQualfiedName = classFullyQualfiedName.replace(".java", ""); + // this condition is actually always true + if (classFullyQualfiedName.endsWith(".java")) { + classFullyQualfiedName = + classFullyQualfiedName.substring(0, classFullyQualfiedName.length() - 5); + } + @SuppressWarnings("signature") // since it's the last element of a fully qualified path + @ClassGetSimpleName String simpleName = + classFullyQualfiedName.substring(classFullyQualfiedName.lastIndexOf(".") + 1); + // If this condition is true, this class is a synthetic class initially created to be a + // return type of some synthetic methods, but later javac has found the correct return type + // for that method. + if (typeToChange.containsKey(simpleName)) { + continue; + } if (!finder.getUsedClass().contains(classFullyQualfiedName)) { continue; } diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index 79a74a78..30e4d36e 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -4,6 +4,7 @@ import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.FieldAccessExpr; @@ -207,6 +208,21 @@ public Visitable visit(MethodDeclaration method, Void p) { return result; } + @Override + public Visitable visit(Parameter para, Void p) { + if (insideTargetMethod) { + ResolvedType paraType = para.resolve().getType(); + if (paraType.isReferenceType()) { + String paraName = paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); + usedClass.add(paraName); + for (ResolvedType parameter : paraType.asReferenceType().typeParametersValues()) { + usedClass.add(parameter.describe()); + } + } + } + return super.visit(para, p); + } + @Override public Visitable visit(MethodCallExpr call, Void p) { if (insideTargetMethod) { diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedClass.java b/src/main/java/org/checkerframework/specimin/UnsolvedClass.java index 08792714..c7db88f1 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedClass.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedClass.java @@ -27,6 +27,9 @@ public class UnsolvedClass { */ private final String packageName; + /** This field checks if this class has a placeholder */ + private boolean hasAPlaceHolder = false; + /** * Create an instance of UnsolvedClass * @@ -95,6 +98,19 @@ public void addFields(String variableExpression) { this.classFields.add(variableExpression); } + /** This method sets hasAPlaceHolder to true */ + public void setPlaceHolder() { + this.hasAPlaceHolder = true; + } + + /** + * This method tells if the current class has a placeholder. + * + * @return true if the current class has a place holder. + */ + public boolean hasPlaceHolder() { + return this.hasAPlaceHolder; + } /** * Update the return type of a method. Note: this method is supposed to be used to update * synthetic methods, where the return type of each method is distinct. @@ -149,7 +165,11 @@ public void updateFieldByType(String currentType, String correctType) { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("package ").append(packageName).append(";\n"); - sb.append("public class ").append(className).append(" {\n"); + if (hasAPlaceHolder) { + sb.append("public class ").append(className).append(" {\n"); + } else { + sb.append("public class ").append(className).append(" {\n"); + } for (String variableDeclarations : classFields) { sb.append(" " + "public " + variableDeclarations + ";\n"); } diff --git a/src/test/java/org/checkerframework/specimin/UnsolvedParameter.java b/src/test/java/org/checkerframework/specimin/UnsolvedParameter.java new file mode 100644 index 00000000..575d0c72 --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/UnsolvedParameter.java @@ -0,0 +1,15 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** This test checks if Specimin can work for tricky, unsolved parameters. */ +public class UnsolvedParameter { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "unsolvedparameter", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#lengthImportStatement(NodeList)"}); + } +} diff --git a/src/test/resources/unsolvedparameter/expected/com/example/Simple.java b/src/test/resources/unsolvedparameter/expected/com/example/Simple.java new file mode 100644 index 00000000..2ad691c8 --- /dev/null +++ b/src/test/resources/unsolvedparameter/expected/com/example/Simple.java @@ -0,0 +1,12 @@ +package com.example; + +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.ImportDeclaration; + +public class Simple { + + public void lengthImportStatement(NodeList listOfImports) { + for (ImportDeclaration importDecl : listOfImports) { + } + } +} diff --git a/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/ImportDeclaration.java b/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/ImportDeclaration.java new file mode 100644 index 00000000..352f1e3d --- /dev/null +++ b/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/ImportDeclaration.java @@ -0,0 +1,4 @@ +package com.github.javaparser.ast; + +public class ImportDeclaration { +} diff --git a/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/NodeList.java b/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/NodeList.java new file mode 100644 index 00000000..8dade271 --- /dev/null +++ b/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/NodeList.java @@ -0,0 +1,4 @@ +package com.github.javaparser.ast; + +public class NodeList { +} diff --git a/src/test/resources/unsolvedparameter/input/com/example/Simple.java b/src/test/resources/unsolvedparameter/input/com/example/Simple.java new file mode 100644 index 00000000..97229476 --- /dev/null +++ b/src/test/resources/unsolvedparameter/input/com/example/Simple.java @@ -0,0 +1,13 @@ +package com.example; + +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.ImportDeclaration; + +public class Simple { + + public void lengthImportStatement(NodeList listOfImports) { + for (ImportDeclaration importDecl: listOfImports) { + + } + } +} \ No newline at end of file From ec4807f735a272e81a39a999d56636882bec731a Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Nov 2023 21:24:54 -0500 Subject: [PATCH 02/23] codes to handle parameters --- .../specimin/UnsolvedSymbolVisitor.java | 110 +++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 367ee8ef..d279a5c0 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -32,6 +32,7 @@ import com.github.javaparser.ast.visitor.Visitable; import com.github.javaparser.resolution.UnsolvedSymbolException; import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedParameterDeclaration; import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; import com.github.javaparser.resolution.types.ResolvedReferenceType; import com.github.javaparser.resolution.types.ResolvedType; @@ -263,6 +264,21 @@ public Visitable visit(PackageDeclaration node, Void arg) { return super.visit(node, arg); } + @Override + public Node visit(ImportDeclaration decl, Void arg) { + @SuppressWarnings( + "signature") // since this is the content of an import statement, it will be a qualified + // class path + @FullyQualifiedName String declAsString = decl.getNameAsString(); + // if there is no jar paths included, every imported class is unsolved, apart from those + // belonging to the Java language. + if (!classesFromJar.isEmpty() || declAsString.startsWith("java")) { + return super.visit(decl, arg); + } + updateMissingClass(getSimpleSyntheticClassFromFullyQualifiedName(declAsString)); + return super.visit(decl, arg); + } + @Override public Visitable visit(ClassOrInterfaceDeclaration node, Void arg) { SimpleName nodeName = node.getName(); @@ -400,6 +416,7 @@ public Visitable visit(BlockStmt node, Void p) { @Override public Visitable visit(VariableDeclarator decl, Void p) { + // This part is to update the symbol table. boolean isAField = !decl.getParentNode().isEmpty() && (decl.getParentNode().get() instanceof FieldDeclaration); if (!isAField) { @@ -407,6 +424,35 @@ public Visitable visit(VariableDeclarator decl, Void p) { currentListOfLocals.add(decl.getNameAsString()); localVariables.addFirst(currentListOfLocals); } + + // This part is to create synthetic class for the type of decl if needed. + Type declType = decl.getType(); + try { + declType.resolve(); + } catch (UnsolvedSymbolException | UnsupportedOperationException e) { + String typeAsString = declType.asString(); + List elements = Splitter.onPattern("\\.").splitToList(typeAsString); + // There could be two cases here: either a fully-qualified class name or a simple class name. + // This is the fully-qualified case. + if (elements.size() > 1) { + @SuppressWarnings("signature") // since this type is in a fully-qualfied form + @FullyQualifiedName String qualifiedTypeName = typeAsString; + updateMissingClass(getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName)); + } + /** + * Handles the case where the type is a simple class name. Two sub-cases are considered: 1. + * The class is included among the import statements. 2. The class is not included in the + * import statements but is in the same directory as the input class. The first sub-case is + * addressed by the visit method for ImportDeclaration. + */ + else if (!classAndPackageMap.containsKey(typeAsString)) { + @SuppressWarnings("signature") // since this is the simple name case + @ClassGetSimpleName String className = typeAsString; + String packageName = this.currentPackage; + UnsolvedClass newClass = new UnsolvedClass(className, packageName); + updateMissingClass(newClass); + } + } return super.visit(decl, p); } @@ -493,7 +539,7 @@ public Visitable visit(MethodDeclaration node, Void arg) { } } - HashSet currentLocalVariables = new HashSet<>(); + HashSet currentLocalVariables = getParameterFromAMethodDeclaration(node); localVariables.addFirst(currentLocalVariables); super.visit(node, arg); localVariables.removeFirst(); @@ -544,8 +590,28 @@ public Visitable visit(MethodCallExpr method, Void p) { @Override public Visitable visit(Parameter parameter, Void p) { try { - parameter.resolve().describeType(); - return super.visit(parameter, p); + ResolvedParameterDeclaration resolvedParameter = parameter.resolve(); + // Specimin will update synthetic class based on import statements before working on + // parameters. Why a parameter could be resolved but not described? Specimin creates the + // synthetic class based on the import statements, so if the synthetic class indeed needs a + // placeholder, Specimin will miss it. + try { + resolvedParameter.describeType(); + return super.visit(parameter, p); + } catch (IllegalArgumentException e) { + // following the above explanation, typeName will be in this form path.to.A + String typeName = parameter.getType().asString(); + @SuppressWarnings( + "signature") // since this type is included among the import statements, we can expect + // that when it is used, it will be in its simple form. + @ClassGetSimpleName String typeToAddPlaceHolder = typeName.substring(0, typeName.indexOf("<")); + UnsolvedClass updateClass = + new UnsolvedClass( + typeToAddPlaceHolder, + classAndPackageMap.getOrDefault(typeToAddPlaceHolder, currentPackage)); + updateClass.setPlaceHolder(); + updateMissingClass(updateClass); + } } // If the parameter originates from a Java built-in library, such as java.io or java.lang, // an UnsupportedOperationException will be thrown instead. @@ -639,6 +705,26 @@ public static String setInitialValueForVariableDeclaration( } } + /** + * Given a parameter in this form "path.to.A," this method will update the placeholder + * for the synthetic class file of type A. This method should only be called in the context of + * visiting parameters. + * + * @param parameter a parameter in form "path.to.A" + */ + public void updatePlaceHolderForParameter(@FullyQualifiedName String parameter) { + // parameter will be in this form: "path.to.A" + // this one might not be a good check but it should work fine. + if (parameter.indexOf("<") > 0) { + System.out.println("We got this parameter: " + parameter); + @SuppressWarnings("signature") // since path.to.A is a qualified name + @FullyQualifiedName String withoutInsidePart = parameter.substring(0, parameter.indexOf("<")); + UnsolvedClass updatedClass = getSimpleSyntheticClassFromFullyQualifiedName(withoutInsidePart); + updatedClass.setPlaceHolder(); + updateMissingClass(updatedClass); + } + } + /** * Given a class name that can either be fully-qualified or simple, this method will convert that * class name to a simple name. @@ -823,6 +909,21 @@ public static boolean isASuperCall(Expression node) { } } + /** + * Given a method declaration, this method will return the set of parameters of that method + * declaration. + * + * @param decl the method declaration + * @return the set of parameters of decl + */ + public HashSet getParameterFromAMethodDeclaration(MethodDeclaration decl) { + HashSet setOfParameters = new HashSet<>(); + for (Parameter parameter : decl.getParameters()) { + setOfParameters.add(parameter.getName().asString()); + } + return setOfParameters; + } + /** * For a super call, this method will update the corresponding synthetic class * @@ -1110,6 +1211,9 @@ public void updateMissingClass(UnsolvedClass missedClass) { for (String variablesDescription : missedClass.getClassFields()) { e.addFields(variablesDescription); } + if (missedClass.hasPlaceHolder()) { + e.setPlaceHolder(); + } return; } } From 348aaa590a07aee6e92eaee989ca1e5e2a7c219c Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Nov 2023 21:27:39 -0500 Subject: [PATCH 03/23] remove debugging line --- .../org/checkerframework/specimin/UnsolvedSymbolVisitor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index d279a5c0..b2740c7b 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -716,7 +716,6 @@ public void updatePlaceHolderForParameter(@FullyQualifiedName String parameter) // parameter will be in this form: "path.to.A" // this one might not be a good check but it should work fine. if (parameter.indexOf("<") > 0) { - System.out.println("We got this parameter: " + parameter); @SuppressWarnings("signature") // since path.to.A is a qualified name @FullyQualifiedName String withoutInsidePart = parameter.substring(0, parameter.indexOf("<")); UnsolvedClass updatedClass = getSimpleSyntheticClassFromFullyQualifiedName(withoutInsidePart); From bb163cc6c52127e2329ddec4805dbbdd0c1fd76e Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Nov 2023 21:35:22 -0500 Subject: [PATCH 04/23] remove < in javadoc --- .../checkerframework/specimin/UnsolvedSymbolVisitor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index b2740c7b..787523bc 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -706,11 +706,11 @@ public static String setInitialValueForVariableDeclaration( } /** - * Given a parameter in this form "path.to.A," this method will update the placeholder - * for the synthetic class file of type A. This method should only be called in the context of + * Given a parameter with a type that has placeholder, this method will update the placeholder for + * the synthetic class file of type A. This method should only be called in the context of * visiting parameters. * - * @param parameter a parameter in form "path.to.A" + * @param parameter a parameter with placeholder in this type */ public void updatePlaceHolderForParameter(@FullyQualifiedName String parameter) { // parameter will be in this form: "path.to.A" From 3801117228c223134e3c843754ddcf59271fc1c1 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Nov 2023 21:51:21 -0500 Subject: [PATCH 05/23] remove unused method --- .../specimin/UnsolvedSymbolVisitor.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 787523bc..7f19b5e1 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -705,25 +705,6 @@ public static String setInitialValueForVariableDeclaration( } } - /** - * Given a parameter with a type that has placeholder, this method will update the placeholder for - * the synthetic class file of type A. This method should only be called in the context of - * visiting parameters. - * - * @param parameter a parameter with placeholder in this type - */ - public void updatePlaceHolderForParameter(@FullyQualifiedName String parameter) { - // parameter will be in this form: "path.to.A" - // this one might not be a good check but it should work fine. - if (parameter.indexOf("<") > 0) { - @SuppressWarnings("signature") // since path.to.A is a qualified name - @FullyQualifiedName String withoutInsidePart = parameter.substring(0, parameter.indexOf("<")); - UnsolvedClass updatedClass = getSimpleSyntheticClassFromFullyQualifiedName(withoutInsidePart); - updatedClass.setPlaceHolder(); - updateMissingClass(updatedClass); - } - } - /** * Given a class name that can either be fully-qualified or simple, this method will convert that * class name to a simple name. From 95968ff2c2ea3769285f764a19133af9ac4d04c0 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Nov 2023 22:03:32 -0500 Subject: [PATCH 06/23] re-add anonymousclass --- .../anonymousclass/expected/com/nameless/SomeClass.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java b/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java index 8dd9312d..895a5721 100644 --- a/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java +++ b/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java @@ -2,11 +2,11 @@ public class SomeClass { - public SomeClass() { + public int getLocalVar() { throw new Error(); } - public int getLocalVar() { + public SomeClass() { throw new Error(); } } From 0c4b7af177bcd17fe945f8148346dec92e70648a Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Nov 2023 12:08:37 -0500 Subject: [PATCH 07/23] codes and test cases for multi type variables --- .../specimin/TargetMethodFinderVisitor.java | 10 +- .../specimin/UnsolvedClass.java | 46 +++++-- .../specimin/UnsolvedSymbolVisitor.java | 127 +++++++++++++----- .../specimin/UnsolvedParameter.java | 2 +- .../expected/com/example/Simple.java | 9 +- .../javaparser/ast/ImportDeclaration.java | 4 - .../com/github/javaparser/ast/NodeList.java | 4 - .../expected/com/name/FullName.java | 4 + .../expected/com/name/LastName.java | 4 + .../expected/com/name/MiddleName.java | 4 + .../input/com/example/Simple.java | 10 +- 11 files changed, 157 insertions(+), 67 deletions(-) delete mode 100644 src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/ImportDeclaration.java delete mode 100644 src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/NodeList.java create mode 100644 src/test/resources/unsolvedparameter/expected/com/name/FullName.java create mode 100644 src/test/resources/unsolvedparameter/expected/com/name/LastName.java create mode 100644 src/test/resources/unsolvedparameter/expected/com/name/MiddleName.java diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index 30e4d36e..af5934ce 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -215,8 +215,14 @@ public Visitable visit(Parameter para, Void p) { if (paraType.isReferenceType()) { String paraName = paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); usedClass.add(paraName); - for (ResolvedType parameter : paraType.asReferenceType().typeParametersValues()) { - usedClass.add(parameter.describe()); + for (ResolvedType typeVariable : paraType.asReferenceType().typeParametersValues()) { + String typeVariableFullName = typeVariable.describe(); + if (typeVariableFullName.contains("<")) { + // removing the "<...>" part if there is any. + typeVariableFullName = + typeVariableFullName.substring(0, typeVariableFullName.indexOf("<")); + } + usedClass.add(typeVariableFullName); } } } diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedClass.java b/src/main/java/org/checkerframework/specimin/UnsolvedClass.java index c7db88f1..4f1d5496 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedClass.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedClass.java @@ -27,8 +27,8 @@ public class UnsolvedClass { */ private final String packageName; - /** This field checks if this class has a placeholder */ - private boolean hasAPlaceHolder = false; + /** This field records the number of type variables for this class */ + private int numberOfTypeVariables = 0; /** * Create an instance of UnsolvedClass @@ -98,19 +98,20 @@ public void addFields(String variableExpression) { this.classFields.add(variableExpression); } - /** This method sets hasAPlaceHolder to true */ - public void setPlaceHolder() { - this.hasAPlaceHolder = true; + /** This method sets the number of type variables for the current class */ + public void setNumberOfTypeVariables(int numberOfTypeVariables) { + this.numberOfTypeVariables = numberOfTypeVariables; } /** - * This method tells if the current class has a placeholder. + * This method tells the number of type variables for this class * - * @return true if the current class has a place holder. + * @return the number of type variables */ - public boolean hasPlaceHolder() { - return this.hasAPlaceHolder; + public int getNumberOfTypeVariables() { + return this.numberOfTypeVariables; } + /** * Update the return type of a method. Note: this method is supposed to be used to update * synthetic methods, where the return type of each method is distinct. @@ -165,11 +166,7 @@ public void updateFieldByType(String currentType, String correctType) { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("package ").append(packageName).append(";\n"); - if (hasAPlaceHolder) { - sb.append("public class ").append(className).append(" {\n"); - } else { - sb.append("public class ").append(className).append(" {\n"); - } + sb.append("public class ").append(className).append(getTypeVariablesAsString()).append(" {\n"); for (String variableDeclarations : classFields) { sb.append(" " + "public " + variableDeclarations + ";\n"); } @@ -179,4 +176,25 @@ public String toString() { sb.append("}\n"); return sb.toString(); } + + /** + * Return a synthetic representation for type variables of the current class. + * + * @return the synthetic representation for type variables + */ + public String getTypeVariablesAsString() { + if (numberOfTypeVariables == 0) { + return ""; + } + StringBuilder result = new StringBuilder(); + // if class A has three type variables, the expression will be A + result.append("<"); + for (int i = 0; i < numberOfTypeVariables; i++) { + String typeExpression = "T" + "T".repeat(i); + result.append(typeExpression).append(", "); + } + result.delete(result.length() - 2, result.length()); + result.append(">"); + return result.toString(); + } } diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 7f19b5e1..28ff421a 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -25,6 +25,7 @@ import com.github.javaparser.ast.stmt.SwitchEntry; import com.github.javaparser.ast.stmt.TryStmt; import com.github.javaparser.ast.stmt.WhileStmt; +import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.type.PrimitiveType; import com.github.javaparser.ast.type.ReferenceType; import com.github.javaparser.ast.type.Type; @@ -32,7 +33,6 @@ import com.github.javaparser.ast.visitor.Visitable; import com.github.javaparser.resolution.UnsolvedSymbolException; import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedParameterDeclaration; import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; import com.github.javaparser.resolution.types.ResolvedReferenceType; import com.github.javaparser.resolution.types.ResolvedType; @@ -264,6 +264,7 @@ public Visitable visit(PackageDeclaration node, Void arg) { return super.visit(node, arg); } + /* @Override public Node visit(ImportDeclaration decl, Void arg) { @SuppressWarnings( @@ -272,13 +273,22 @@ public Node visit(ImportDeclaration decl, Void arg) { @FullyQualifiedName String declAsString = decl.getNameAsString(); // if there is no jar paths included, every imported class is unsolved, apart from those // belonging to the Java language. - if (!classesFromJar.isEmpty() || declAsString.startsWith("java")) { + if (!classesFromJar.isEmpty() || declAsString.startsWith("java.")) { + return super.visit(decl, arg); + } + UnsolvedClass declaredClass = getSimpleSyntheticClassFromFullyQualifiedName(declAsString); + try { + createMissingClass(declaredClass); + missingClass.add(declaredClass); + } catch (RuntimeException e) { + // The class file already exists in the codebase return super.visit(decl, arg); } - updateMissingClass(getSimpleSyntheticClassFromFullyQualifiedName(declAsString)); return super.visit(decl, arg); } + */ + @Override public Visitable visit(ClassOrInterfaceDeclaration node, Void arg) { SimpleName nodeName = node.getName(); @@ -532,6 +542,7 @@ public Visitable visit(MethodDeclaration node, Void arg) { // since this method declaration is inside an anonymous class, its parent will be an // ObjectCreationExpr ((ObjectCreationExpr) parentNode).resolve(); + nodeType.resolve(); } catch (UnsolvedSymbolException | UnsupportedOperationException e) { SimpleName classNodeSimpleName = ((ObjectCreationExpr) parentNode).getType().getName(); String nameOfClass = classNodeSimpleName.asString(); @@ -588,30 +599,60 @@ public Visitable visit(MethodCallExpr method, Void p) { } @Override - public Visitable visit(Parameter parameter, Void p) { + public Visitable visit(ClassOrInterfaceType typeExpr, Void p) { + // this line is to work around a bug of JavaParser. When a type is called as a class path, such + // as com.example.Dog dog, JavaParser will also consider com and com.example as type name. This + // bug happens even if the source file of class Dog is present in the codebase. + if (!isCapital(typeExpr.getName().asString())) { + return super.visit(typeExpr, p); + } + if (!typeExpr.isReferenceType()) { + return super.visit(typeExpr, p); + } try { - ResolvedParameterDeclaration resolvedParameter = parameter.resolve(); - // Specimin will update synthetic class based on import statements before working on - // parameters. Why a parameter could be resolved but not described? Specimin creates the - // synthetic class based on the import statements, so if the synthetic class indeed needs a - // placeholder, Specimin will miss it. - try { - resolvedParameter.describeType(); - return super.visit(parameter, p); - } catch (IllegalArgumentException e) { - // following the above explanation, typeName will be in this form path.to.A - String typeName = parameter.getType().asString(); - @SuppressWarnings( - "signature") // since this type is included among the import statements, we can expect - // that when it is used, it will be in its simple form. - @ClassGetSimpleName String typeToAddPlaceHolder = typeName.substring(0, typeName.indexOf("<")); - UnsolvedClass updateClass = - new UnsolvedClass( - typeToAddPlaceHolder, - classAndPackageMap.getOrDefault(typeToAddPlaceHolder, currentPackage)); - updateClass.setPlaceHolder(); - updateMissingClass(updateClass); + typeExpr.getElementType().resolve().describe(); + return super.visit(typeExpr, p); + } + /* + * If the class file is not in the codebase yet, we got UnsolvedSymbolException. + * If the class file is not in the codebase and used by an anonymous class, we got UnsupportedOperationException. + * If the class file is in the codebase but the type variables are missing, we got IllegalArgumentException. + */ + catch (UnsolvedSymbolException | UnsupportedOperationException | IllegalArgumentException e) { + // This method only updates type variables for unsolved classes. Other problems causing a + // class to be unsolved will be fixed by other methods. + Optional> typeArguments = typeExpr.getTypeArguments(); + UnsolvedClass classToUpdate; + int numberOfArguments = 0; + String typeRawName = typeExpr.getElementType().asString(); + if (typeArguments.isPresent()) { + numberOfArguments = typeArguments.get().size(); + // without any type argument + typeRawName = typeRawName.substring(0, typeRawName.indexOf("<")); } + if (isAClassPath(typeRawName)) { + String packageName = typeRawName.substring(0, typeRawName.lastIndexOf(".")); + @SuppressWarnings("signature") // since this is the last element of a class path + @ClassGetSimpleName String className = typeRawName.substring(typeRawName.lastIndexOf(".") + 1); + classToUpdate = new UnsolvedClass(className, packageName); + } else { + @SuppressWarnings("signature") // since it is not in a fully qualifed format + @ClassGetSimpleName String className = typeRawName; + String packageName = classAndPackageMap.getOrDefault(className, currentPackage); + classToUpdate = new UnsolvedClass(className, packageName); + } + classToUpdate.setNumberOfTypeVariables(numberOfArguments); + updateMissingClass(classToUpdate); + gotException = true; + } + return super.visit(typeExpr, p); + } + + @Override + public Visitable visit(Parameter parameter, Void p) { + try { + parameter.resolve(); + return super.visit(parameter, p); } // If the parameter originates from a Java built-in library, such as java.io or java.lang, // an UnsupportedOperationException will be thrown instead. @@ -659,6 +700,7 @@ public Visitable visit(ObjectCreationExpr newExpr, Void p) { UnsolvedClass newClass = new UnsolvedClass(type, classAndPackageMap.get(type)); newClass.addMethod(creationMethod); this.updateMissingClass(newClass); + System.out.println("I was called by 00000"); } else { throw new RuntimeException("Unexpected class: " + type); } @@ -776,6 +818,7 @@ public void updateUnsolvedClassWithMethod( UnsolvedClass returnTypeForThisMethod = new UnsolvedClass(returnType, missingClass.getPackageName()); this.updateMissingClass(returnTypeForThisMethod); + System.out.println("I was called by 2114124"); classAndPackageMap.put( returnTypeForThisMethod.getClassName(), returnTypeForThisMethod.getPackageName()); } @@ -835,6 +878,7 @@ public void updateClassesFromJarSourcesForMethodCall(MethodCallExpr expr) { UnsolvedMethod thisMethod = new UnsolvedMethod(methodName, returnType, argumentsList); missingClass.addMethod(thisMethod); syntheticMethodAndClass.put(methodName, missingClass); + System.out.println("I was called by update class from Jar Sources"); this.updateMissingClass(missingClass); } @@ -964,6 +1008,7 @@ public void updateUnsolvedClassWithFields( setInitialValueForVariableDeclaration(variableType, variableType + " " + var)); updateMissingClass(relatedClass); updateMissingClass(varType); + System.out.println("I was called by updateUnsolvdClassWithFields"); } } @@ -1191,8 +1236,8 @@ public void updateMissingClass(UnsolvedClass missedClass) { for (String variablesDescription : missedClass.getClassFields()) { e.addFields(variablesDescription); } - if (missedClass.hasPlaceHolder()) { - e.setPlaceHolder(); + if (missedClass.getNumberOfTypeVariables() > 0) { + e.setNumberOfTypeVariables(missedClass.getNumberOfTypeVariables()); } return; } @@ -1220,10 +1265,12 @@ public void updateSyntheticSourceCode() { * @param missedClass a synthetic class to be deleted */ public void deleteOldSyntheticClass(UnsolvedClass missedClass) { - String classPackage = classAndPackageMap.get(missedClass.getClassName()); + String classPackage = missedClass.getPackageName(); + String classDirectory = classPackage.replace(".", "/"); String filePathStr = - this.rootDirectory + classPackage + "/" + missedClass.getClassName() + ".java"; + this.rootDirectory + classDirectory + "/" + missedClass.getClassName() + ".java"; Path filePath = Path.of(filePathStr); + System.out.println("Deleted file: " + filePath); try { Files.delete(filePath); } catch (IOException e) { @@ -1246,6 +1293,14 @@ public void createMissingClass(UnsolvedClass missedClass) { String filePathStr = this.rootDirectory + classDirectory + "/" + missedClass.getClassName() + ".java"; Path filePath = Paths.get(filePathStr); + + // When class paths are not provide, this visitor will attempt to create synthetic classes for + // every class imported by the import statements. However, some of those classes might already + // exist in the input codebase. + if (Files.exists(filePath)) { + throw new RuntimeException("File already exists: " + filePath); + } + createdClass.add(filePath); try { Path parentPath = filePath.getParent(); @@ -1255,6 +1310,7 @@ public void createMissingClass(UnsolvedClass missedClass) { try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toFile(), StandardCharsets.UTF_8))) { writer.write(fileContent.toString()); + System.out.println(fileContent); } catch (Exception e) { throw new Error(e.getMessage()); } @@ -1433,6 +1489,7 @@ public void updateClassSetWithQualifiedStaticMethodCall( syntheticReturnTypes.add(returnTypeFullName); this.updateMissingClass(returnClass); this.updateMissingClass(classThatContainMethod); + System.out.println("I was called by adasdada"); } /** @@ -1450,15 +1507,23 @@ public void updateTypes( + incorrectType.substring(1).replace("ReturnType", ""); UnsolvedClass relatedClass = syntheticMethodAndClass.get(involvedMethod); if (relatedClass != null) { + for (UnsolvedClass syntheticClass : missingClass) { + if (syntheticClass.getClassName().equals(relatedClass.getClassName()) + && syntheticClass.getPackageName().equals(relatedClass.getPackageName())) { + syntheticClass.updateMethodByReturnType( + incorrectType, typeToCorrect.get(incorrectType)); + this.deleteOldSyntheticClass(syntheticClass); + this.createMissingClass(syntheticClass); + } + } relatedClass.updateMethodByReturnType(incorrectType, typeToCorrect.get(incorrectType)); - this.deleteOldSyntheticClass(relatedClass); - this.createMissingClass(relatedClass); } // if the above condition is not met, then this incorrectType is a synthetic type for the // fields of the parent class rather than the return type of some methods else { for (UnsolvedClass unsolClass : missingClass) { for (String parentClass : classAndItsParent.values()) { + System.out.println(unsolClass.getClassName()); if (unsolClass.getClassName().equals(parentClass)) { unsolClass.updateFieldByType(incorrectType, typeToCorrect.get(incorrectType)); this.deleteOldSyntheticClass(unsolClass); diff --git a/src/test/java/org/checkerframework/specimin/UnsolvedParameter.java b/src/test/java/org/checkerframework/specimin/UnsolvedParameter.java index 575d0c72..78325f87 100644 --- a/src/test/java/org/checkerframework/specimin/UnsolvedParameter.java +++ b/src/test/java/org/checkerframework/specimin/UnsolvedParameter.java @@ -10,6 +10,6 @@ public void runTest() throws IOException { SpeciminTestExecutor.runTestWithoutJarPaths( "unsolvedparameter", new String[] {"com/example/Simple.java"}, - new String[] {"com.example.Simple#lengthImportStatement(NodeList)"}); + new String[] {"com.example.Simple#printName(FullName, LastName>)"}); } } diff --git a/src/test/resources/unsolvedparameter/expected/com/example/Simple.java b/src/test/resources/unsolvedparameter/expected/com/example/Simple.java index 2ad691c8..4f7d709d 100644 --- a/src/test/resources/unsolvedparameter/expected/com/example/Simple.java +++ b/src/test/resources/unsolvedparameter/expected/com/example/Simple.java @@ -1,12 +1,11 @@ package com.example; -import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.ImportDeclaration; +import com.name.FullName; +import com.name.MiddleName; +import com.name.LastName; public class Simple { - public void lengthImportStatement(NodeList listOfImports) { - for (ImportDeclaration importDecl : listOfImports) { - } + public void printName(FullName, LastName> fullName) { } } diff --git a/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/ImportDeclaration.java b/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/ImportDeclaration.java deleted file mode 100644 index 352f1e3d..00000000 --- a/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/ImportDeclaration.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.javaparser.ast; - -public class ImportDeclaration { -} diff --git a/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/NodeList.java b/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/NodeList.java deleted file mode 100644 index 8dade271..00000000 --- a/src/test/resources/unsolvedparameter/expected/com/github/javaparser/ast/NodeList.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.javaparser.ast; - -public class NodeList { -} diff --git a/src/test/resources/unsolvedparameter/expected/com/name/FullName.java b/src/test/resources/unsolvedparameter/expected/com/name/FullName.java new file mode 100644 index 00000000..aabf42f7 --- /dev/null +++ b/src/test/resources/unsolvedparameter/expected/com/name/FullName.java @@ -0,0 +1,4 @@ +package com.name; + +public class FullName { +} diff --git a/src/test/resources/unsolvedparameter/expected/com/name/LastName.java b/src/test/resources/unsolvedparameter/expected/com/name/LastName.java new file mode 100644 index 00000000..9fbf0f24 --- /dev/null +++ b/src/test/resources/unsolvedparameter/expected/com/name/LastName.java @@ -0,0 +1,4 @@ +package com.name; + +public class LastName { +} diff --git a/src/test/resources/unsolvedparameter/expected/com/name/MiddleName.java b/src/test/resources/unsolvedparameter/expected/com/name/MiddleName.java new file mode 100644 index 00000000..efba7e9b --- /dev/null +++ b/src/test/resources/unsolvedparameter/expected/com/name/MiddleName.java @@ -0,0 +1,4 @@ +package com.name; + +public class MiddleName { +} diff --git a/src/test/resources/unsolvedparameter/input/com/example/Simple.java b/src/test/resources/unsolvedparameter/input/com/example/Simple.java index 97229476..8d71d844 100644 --- a/src/test/resources/unsolvedparameter/input/com/example/Simple.java +++ b/src/test/resources/unsolvedparameter/input/com/example/Simple.java @@ -1,13 +1,11 @@ package com.example; -import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.ImportDeclaration; +import com.name.FullName; +import com.name.MiddleName; +import com.name.LastName; public class Simple { + public void printName (FullName, LastName> fullName) { - public void lengthImportStatement(NodeList listOfImports) { - for (ImportDeclaration importDecl: listOfImports) { - - } } } \ No newline at end of file From 7666b1b93e30f2486ca43b1d7813113f04fc1c72 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Nov 2023 12:15:05 -0500 Subject: [PATCH 08/23] some cleaning --- .../specimin/TargetMethodFinderVisitor.java | 4 ++-- .../checkerframework/specimin/UnsolvedSymbolVisitor.java | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index af5934ce..14f6445b 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -213,8 +213,8 @@ public Visitable visit(Parameter para, Void p) { if (insideTargetMethod) { ResolvedType paraType = para.resolve().getType(); if (paraType.isReferenceType()) { - String paraName = paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); - usedClass.add(paraName); + String paraTypeFullName = paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); + usedClass.add(paraTypeFullName); for (ResolvedType typeVariable : paraType.asReferenceType().typeParametersValues()) { String typeVariableFullName = typeVariable.describe(); if (typeVariableFullName.contains("<")) { diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 28ff421a..050b5d44 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -700,7 +700,6 @@ public Visitable visit(ObjectCreationExpr newExpr, Void p) { UnsolvedClass newClass = new UnsolvedClass(type, classAndPackageMap.get(type)); newClass.addMethod(creationMethod); this.updateMissingClass(newClass); - System.out.println("I was called by 00000"); } else { throw new RuntimeException("Unexpected class: " + type); } @@ -818,7 +817,6 @@ public void updateUnsolvedClassWithMethod( UnsolvedClass returnTypeForThisMethod = new UnsolvedClass(returnType, missingClass.getPackageName()); this.updateMissingClass(returnTypeForThisMethod); - System.out.println("I was called by 2114124"); classAndPackageMap.put( returnTypeForThisMethod.getClassName(), returnTypeForThisMethod.getPackageName()); } @@ -878,7 +876,6 @@ public void updateClassesFromJarSourcesForMethodCall(MethodCallExpr expr) { UnsolvedMethod thisMethod = new UnsolvedMethod(methodName, returnType, argumentsList); missingClass.addMethod(thisMethod); syntheticMethodAndClass.put(methodName, missingClass); - System.out.println("I was called by update class from Jar Sources"); this.updateMissingClass(missingClass); } @@ -1008,7 +1005,6 @@ public void updateUnsolvedClassWithFields( setInitialValueForVariableDeclaration(variableType, variableType + " " + var)); updateMissingClass(relatedClass); updateMissingClass(varType); - System.out.println("I was called by updateUnsolvdClassWithFields"); } } @@ -1270,7 +1266,6 @@ public void deleteOldSyntheticClass(UnsolvedClass missedClass) { String filePathStr = this.rootDirectory + classDirectory + "/" + missedClass.getClassName() + ".java"; Path filePath = Path.of(filePathStr); - System.out.println("Deleted file: " + filePath); try { Files.delete(filePath); } catch (IOException e) { @@ -1310,7 +1305,6 @@ public void createMissingClass(UnsolvedClass missedClass) { try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toFile(), StandardCharsets.UTF_8))) { writer.write(fileContent.toString()); - System.out.println(fileContent); } catch (Exception e) { throw new Error(e.getMessage()); } @@ -1489,7 +1483,6 @@ public void updateClassSetWithQualifiedStaticMethodCall( syntheticReturnTypes.add(returnTypeFullName); this.updateMissingClass(returnClass); this.updateMissingClass(classThatContainMethod); - System.out.println("I was called by adasdada"); } /** @@ -1523,7 +1516,6 @@ public void updateTypes( else { for (UnsolvedClass unsolClass : missingClass) { for (String parentClass : classAndItsParent.values()) { - System.out.println(unsolClass.getClassName()); if (unsolClass.getClassName().equals(parentClass)) { unsolClass.updateFieldByType(incorrectType, typeToCorrect.get(incorrectType)); this.deleteOldSyntheticClass(unsolClass); From e601238e7bf7843a258c7a8f54729c36fde71ca9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Nov 2023 12:19:12 -0500 Subject: [PATCH 09/23] add throw --- .../org/checkerframework/specimin/SpeciminRunner.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java index 5450d30e..2c88073e 100644 --- a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java +++ b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java @@ -190,11 +190,11 @@ public static void performMinimization( if (isEmptyCompilationUnit(target.getValue())) { // target key will have this form: "path/of/package/ClassName.java" String classFullyQualfiedName = target.getKey().replace("/", "."); - // this condition is actually always true - if (classFullyQualfiedName.endsWith(".java")) { - classFullyQualfiedName = - classFullyQualfiedName.substring(0, classFullyQualfiedName.length() - 5); + if (!classFullyQualfiedName.endsWith(".java")) { + throw new RuntimeException("A Java file directory does not end with .java: " + classFullyQualfiedName); } + classFullyQualfiedName = + classFullyQualfiedName.substring(0, classFullyQualfiedName.length() - 5); @SuppressWarnings("signature") // since it's the last element of a fully qualified path @ClassGetSimpleName String simpleName = classFullyQualfiedName.substring(classFullyQualfiedName.lastIndexOf(".") + 1); From e2188c24d6c8cde306d957f943cac7b9646753eb Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Nov 2023 12:31:46 -0500 Subject: [PATCH 10/23] some more cleaning and clarification --- .../specimin/SpeciminRunner.java | 11 ++--- .../specimin/TargetMethodFinderVisitor.java | 3 +- .../specimin/UnsolvedSymbolVisitor.java | 42 +++---------------- 3 files changed, 14 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java index 2c88073e..c22ce570 100644 --- a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java +++ b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java @@ -166,9 +166,9 @@ public static void performMinimization( // correct the types of all related files before adding them to parsedTargetFiles JavaTypeCorrect typeCorrecter = new JavaTypeCorrect(root, relatedClass); typeCorrecter.correctTypesForAllFiles(); - Map<@ClassGetSimpleName String, @ClassGetSimpleName String> typeToChange = + Map<@ClassGetSimpleName String, @ClassGetSimpleName String> typesToChange = typeCorrecter.getTypeToChange(); - addMissingClass.updateTypes(typeToChange); + addMissingClass.updateTypes(typesToChange); for (String directory : relatedClass) { // directories already in parsedTargetFiles are original files in the root directory, we are @@ -191,17 +191,18 @@ public static void performMinimization( // target key will have this form: "path/of/package/ClassName.java" String classFullyQualfiedName = target.getKey().replace("/", "."); if (!classFullyQualfiedName.endsWith(".java")) { - throw new RuntimeException("A Java file directory does not end with .java: " + classFullyQualfiedName); + throw new RuntimeException( + "A Java file directory does not end with .java: " + classFullyQualfiedName); } classFullyQualfiedName = - classFullyQualfiedName.substring(0, classFullyQualfiedName.length() - 5); + classFullyQualfiedName.substring(0, classFullyQualfiedName.length() - 5); @SuppressWarnings("signature") // since it's the last element of a fully qualified path @ClassGetSimpleName String simpleName = classFullyQualfiedName.substring(classFullyQualfiedName.lastIndexOf(".") + 1); // If this condition is true, this class is a synthetic class initially created to be a // return type of some synthetic methods, but later javac has found the correct return type // for that method. - if (typeToChange.containsKey(simpleName)) { + if (typesToChange.containsKey(simpleName)) { continue; } if (!finder.getUsedClass().contains(classFullyQualfiedName)) { diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index 14f6445b..c2e6d01d 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -213,7 +213,8 @@ public Visitable visit(Parameter para, Void p) { if (insideTargetMethod) { ResolvedType paraType = para.resolve().getType(); if (paraType.isReferenceType()) { - String paraTypeFullName = paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); + String paraTypeFullName = + paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); usedClass.add(paraTypeFullName); for (ResolvedType typeVariable : paraType.asReferenceType().typeParametersValues()) { String typeVariableFullName = typeVariable.describe(); diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 050b5d44..de462e2c 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -264,31 +264,6 @@ public Visitable visit(PackageDeclaration node, Void arg) { return super.visit(node, arg); } - /* - @Override - public Node visit(ImportDeclaration decl, Void arg) { - @SuppressWarnings( - "signature") // since this is the content of an import statement, it will be a qualified - // class path - @FullyQualifiedName String declAsString = decl.getNameAsString(); - // if there is no jar paths included, every imported class is unsolved, apart from those - // belonging to the Java language. - if (!classesFromJar.isEmpty() || declAsString.startsWith("java.")) { - return super.visit(decl, arg); - } - UnsolvedClass declaredClass = getSimpleSyntheticClassFromFullyQualifiedName(declAsString); - try { - createMissingClass(declaredClass); - missingClass.add(declaredClass); - } catch (RuntimeException e) { - // The class file already exists in the codebase - return super.visit(decl, arg); - } - return super.visit(decl, arg); - } - - */ - @Override public Visitable visit(ClassOrInterfaceDeclaration node, Void arg) { SimpleName nodeName = node.getName(); @@ -542,7 +517,6 @@ public Visitable visit(MethodDeclaration node, Void arg) { // since this method declaration is inside an anonymous class, its parent will be an // ObjectCreationExpr ((ObjectCreationExpr) parentNode).resolve(); - nodeType.resolve(); } catch (UnsolvedSymbolException | UnsupportedOperationException e) { SimpleName classNodeSimpleName = ((ObjectCreationExpr) parentNode).getType().getName(); String nameOfClass = classNodeSimpleName.asString(); @@ -600,9 +574,12 @@ public Visitable visit(MethodCallExpr method, Void p) { @Override public Visitable visit(ClassOrInterfaceType typeExpr, Void p) { - // this line is to work around a bug of JavaParser. When a type is called as a class path, such - // as com.example.Dog dog, JavaParser will also consider com and com.example as type name. This - // bug happens even if the source file of class Dog is present in the codebase. + /** + * Workaround for a JavaParser bug: When a type is referenced as a class path, 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())) { return super.visit(typeExpr, p); } @@ -1289,13 +1266,6 @@ public void createMissingClass(UnsolvedClass missedClass) { this.rootDirectory + classDirectory + "/" + missedClass.getClassName() + ".java"; Path filePath = Paths.get(filePathStr); - // When class paths are not provide, this visitor will attempt to create synthetic classes for - // every class imported by the import statements. However, some of those classes might already - // exist in the input codebase. - if (Files.exists(filePath)) { - throw new RuntimeException("File already exists: " + filePath); - } - createdClass.add(filePath); try { Path parentPath = filePath.getParent(); From e130de3128048a7c3e11137f4d4b93fbebc2392b Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Mon, 20 Nov 2023 15:50:40 -0500 Subject: [PATCH 11/23] checkpoint: address all but one cr comment from myself --- .../specimin/SpeciminRunner.java | 27 ++++++++++++++----- .../specimin/TargetMethodFinderVisitor.java | 12 ++++----- .../specimin/UnsolvedClass.java | 4 +-- .../specimin/UnsolvedSymbolVisitor.java | 11 ++++---- .../specimin/TypeVarSimpleTest.java | 15 +++++++++++ .../specimin/UnsolvedStaticMethod.java | 4 +-- .../expected/com/example/TypeVarSimple.java | 10 +++++++ .../org/example/ClassWithTypeParam.java | 7 +++++ .../input/com/example/TypeVarSimple.java | 10 +++++++ 9 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 src/test/java/org/checkerframework/specimin/TypeVarSimpleTest.java create mode 100644 src/test/resources/typevarsimple/expected/com/example/TypeVarSimple.java create mode 100644 src/test/resources/typevarsimple/expected/org/example/ClassWithTypeParam.java create mode 100644 src/test/resources/typevarsimple/input/com/example/TypeVarSimple.java diff --git a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java index c22ce570..ebb1f9d2 100644 --- a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java +++ b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java @@ -27,6 +27,7 @@ import joptsimple.OptionSet; import joptsimple.OptionSpec; import org.checkerframework.checker.signature.qual.ClassGetSimpleName; +import org.checkerframework.checker.signature.qual.FullyQualifiedName; /** This class is the main runner for Specimin. Use its main() method to start Specimin. */ public class SpeciminRunner { @@ -189,13 +190,7 @@ public static void performMinimization( // the target methods, do not output it. if (isEmptyCompilationUnit(target.getValue())) { // target key will have this form: "path/of/package/ClassName.java" - String classFullyQualfiedName = target.getKey().replace("/", "."); - if (!classFullyQualfiedName.endsWith(".java")) { - throw new RuntimeException( - "A Java file directory does not end with .java: " + classFullyQualfiedName); - } - classFullyQualfiedName = - classFullyQualfiedName.substring(0, classFullyQualfiedName.length() - 5); + String classFullyQualfiedName = getFullyQualifiedClassName(target.getKey()); @SuppressWarnings("signature") // since it's the last element of a fully qualified path @ClassGetSimpleName String simpleName = classFullyQualfiedName.substring(classFullyQualfiedName.lastIndexOf(".") + 1); @@ -233,6 +228,24 @@ public static void performMinimization( deleteFiles(createdClass); } + /** + * Converts a path to a Java file into the fully-qualified name of the public class in that file, + * relying on the file's relative path being the same as the package name. + * + * @param javaFilePath the path to a .java file, in this form: "path/of/package/ClassName.java". + * Note that this path must be rooted at the same directory in which javac could be invoked to + * compile the file + * @return the fully-qualified name of the given class + */ + @SuppressWarnings("signature") // string manipulation + private static @FullyQualifiedName String getFullyQualifiedClassName(final String javaFilePath) { + String result = javaFilePath.replace("/", "."); + if (!result.endsWith(".java")) { + throw new RuntimeException("A Java file path does not end with .java: " + result); + } + return result.substring(0, result.length() - 5); + } + /** * Checks whether the given compilation unit contains nothing. Should conservatively return false * by default if unsure. diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index c2e6d01d..ec8f335a 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -216,14 +216,14 @@ public Visitable visit(Parameter para, Void p) { String paraTypeFullName = paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); usedClass.add(paraTypeFullName); - for (ResolvedType typeVariable : paraType.asReferenceType().typeParametersValues()) { - String typeVariableFullName = typeVariable.describe(); - if (typeVariableFullName.contains("<")) { + for (ResolvedType typeParameterValue : paraType.asReferenceType().typeParametersValues()) { + String typeParameterValueName = typeParameterValue.describe(); + if (typeParameterValueName.contains("<")) { // removing the "<...>" part if there is any. - typeVariableFullName = - typeVariableFullName.substring(0, typeVariableFullName.indexOf("<")); + typeParameterValueName = + typeParameterValueName.substring(0, typeParameterValueName.indexOf("<")); } - usedClass.add(typeVariableFullName); + usedClass.add(typeParameterValueName); } } } diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedClass.java b/src/main/java/org/checkerframework/specimin/UnsolvedClass.java index 4f1d5496..a08f1ad2 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedClass.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedClass.java @@ -187,10 +187,10 @@ public String getTypeVariablesAsString() { return ""; } StringBuilder result = new StringBuilder(); - // if class A has three type variables, the expression will be A + // if class A has three type variables, the expression will be A result.append("<"); for (int i = 0; i < numberOfTypeVariables; i++) { - String typeExpression = "T" + "T".repeat(i); + String typeExpression = "T" + ((i > 0) ? i : ""); result.append(typeExpression).append(", "); } result.delete(result.length() - 2, result.length()); diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index de462e2c..813ed99b 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -574,12 +574,11 @@ public Visitable visit(MethodCallExpr method, Void p) { @Override public Visitable visit(ClassOrInterfaceType typeExpr, Void p) { - /** - * Workaround for a JavaParser bug: When a type is referenced as a class path, 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. - */ + // Workaround for a JavaParser bug: When a type is referenced using its fully-qualified name, + // 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())) { return super.visit(typeExpr, p); } diff --git a/src/test/java/org/checkerframework/specimin/TypeVarSimpleTest.java b/src/test/java/org/checkerframework/specimin/TypeVarSimpleTest.java new file mode 100644 index 00000000..a2339c7f --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/TypeVarSimpleTest.java @@ -0,0 +1,15 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** This test checks if Specimin can work for methods that have type parameters themselves. */ +public class TypeVarSimpleTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "typevarsimple", + new String[] {"com/example/TypeVarSimple.java"}, + new String[] {"com.example.TypeVarSimple#methodWithTypeParameter(ClassWithTypeParam)"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/UnsolvedStaticMethod.java b/src/test/java/org/checkerframework/specimin/UnsolvedStaticMethod.java index 6ac079ff..f177be81 100644 --- a/src/test/java/org/checkerframework/specimin/UnsolvedStaticMethod.java +++ b/src/test/java/org/checkerframework/specimin/UnsolvedStaticMethod.java @@ -3,9 +3,7 @@ import java.io.IOException; import org.junit.Test; -/** - * This test checks if Specimin will work for input files that contain unsolved static methods. - */ +/** This test checks if Specimin will work for input files that contain unsolved static methods. */ public class UnsolvedStaticMethod { @Test public void runTest() throws IOException { diff --git a/src/test/resources/typevarsimple/expected/com/example/TypeVarSimple.java b/src/test/resources/typevarsimple/expected/com/example/TypeVarSimple.java new file mode 100644 index 00000000..a1440a8e --- /dev/null +++ b/src/test/resources/typevarsimple/expected/com/example/TypeVarSimple.java @@ -0,0 +1,10 @@ +package com.example; + +import org.example.ClassWithTypeParam; + +public class TypeVarSimple { + public static T methodWithTypeParameter(ClassWithTypeParam param) { + T result = param.getT(); + return result; + } +} diff --git a/src/test/resources/typevarsimple/expected/org/example/ClassWithTypeParam.java b/src/test/resources/typevarsimple/expected/org/example/ClassWithTypeParam.java new file mode 100644 index 00000000..8b17e94a --- /dev/null +++ b/src/test/resources/typevarsimple/expected/org/example/ClassWithTypeParam.java @@ -0,0 +1,7 @@ +package org.example; + +public class ClassWithTypeParam { + T getT() { + throw new Error(); + } +} diff --git a/src/test/resources/typevarsimple/input/com/example/TypeVarSimple.java b/src/test/resources/typevarsimple/input/com/example/TypeVarSimple.java new file mode 100644 index 00000000..a1440a8e --- /dev/null +++ b/src/test/resources/typevarsimple/input/com/example/TypeVarSimple.java @@ -0,0 +1,10 @@ +package com.example; + +import org.example.ClassWithTypeParam; + +public class TypeVarSimple { + public static T methodWithTypeParameter(ClassWithTypeParam param) { + T result = param.getT(); + return result; + } +} From 3c414c21216c89a421d4f039cb6642849b47d27b Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Mon, 20 Nov 2023 16:01:07 -0500 Subject: [PATCH 12/23] add test for lower case class name --- .../specimin/LowercaseClassTest.java | 18 ++++++++++++++++++ .../expected/com/example/simple.java | 7 +++++++ .../input/com/example/simple.java | 7 +++++++ 3 files changed, 32 insertions(+) create mode 100644 src/test/java/org/checkerframework/specimin/LowercaseClassTest.java create mode 100644 src/test/resources/lowercaseclass/expected/com/example/simple.java create mode 100644 src/test/resources/lowercaseclass/input/com/example/simple.java diff --git a/src/test/java/org/checkerframework/specimin/LowercaseClassTest.java b/src/test/java/org/checkerframework/specimin/LowercaseClassTest.java new file mode 100644 index 00000000..363dc72b --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/LowercaseClassTest.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks that a simple Java file with a lowercase class name (against convention but + * allowed!) doesn't cause Specimin any problems. + */ +public class LowercaseClassTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "lowercaseclass", + new String[] {"com/example/simple.java"}, + new String[] {"com.example.simple#test()"}); + } +} diff --git a/src/test/resources/lowercaseclass/expected/com/example/simple.java b/src/test/resources/lowercaseclass/expected/com/example/simple.java new file mode 100644 index 00000000..762f3364 --- /dev/null +++ b/src/test/resources/lowercaseclass/expected/com/example/simple.java @@ -0,0 +1,7 @@ +package com.example; + +class simple { + static void test() { + simple s = new simple(); + } +} diff --git a/src/test/resources/lowercaseclass/input/com/example/simple.java b/src/test/resources/lowercaseclass/input/com/example/simple.java new file mode 100644 index 00000000..762f3364 --- /dev/null +++ b/src/test/resources/lowercaseclass/input/com/example/simple.java @@ -0,0 +1,7 @@ +package com.example; + +class simple { + static void test() { + simple s = new simple(); + } +} From 7d6261b0db17ebc7c17168fcee0b03a088664d48 Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Mon, 20 Nov 2023 16:04:05 -0500 Subject: [PATCH 13/23] fix expected test output --- .../resources/unsolvedparameter/expected/com/name/FullName.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/unsolvedparameter/expected/com/name/FullName.java b/src/test/resources/unsolvedparameter/expected/com/name/FullName.java index aabf42f7..1395df93 100644 --- a/src/test/resources/unsolvedparameter/expected/com/name/FullName.java +++ b/src/test/resources/unsolvedparameter/expected/com/name/FullName.java @@ -1,4 +1,4 @@ package com.name; -public class FullName { +public class FullName { } From 014c1dd7568a3d31672a977fc8ff64e1147ea18c Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Mon, 20 Nov 2023 18:35:14 -0500 Subject: [PATCH 14/23] checkpoint --- build.gradle | 6 ++ .../specimin/UnsolvedSymbolVisitor.java | 95 +++++++++++++++---- 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index 1df1240a..3a8693b2 100644 --- a/build.gradle +++ b/build.gradle @@ -110,3 +110,9 @@ javadoc { options.addBooleanOption('html5', true) } } + +test { + testLogging { + showStandardStreams = true + } +} diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 813ed99b..eb70dc99 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -5,6 +5,7 @@ import com.github.javaparser.ast.NodeList; 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.FieldDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; @@ -29,6 +30,7 @@ import com.github.javaparser.ast.type.PrimitiveType; import com.github.javaparser.ast.type.ReferenceType; import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.type.TypeParameter; import com.github.javaparser.ast.visitor.ModifierVisitor; import com.github.javaparser.ast.visitor.Visitable; import com.github.javaparser.resolution.UnsolvedSymbolException; @@ -83,6 +85,9 @@ public class UnsolvedSymbolVisitor extends ModifierVisitor { /** The symbol table to keep track of local variables in the current input file */ private final ArrayDeque> localVariables = new ArrayDeque<>(); + /** The symbol table for type variables. */ + private final ArrayDeque> typeVariables = new ArrayDeque>(); + /** The simple name of the class currently visited */ private @ClassGetSimpleName String className = ""; @@ -275,7 +280,10 @@ public Visitable visit(ClassOrInterfaceDeclaration node, Void arg) { SimpleName superClassSimpleName = node.getExtendedTypes().get(0).getName(); classAndItsParent.put(className, superClassSimpleName.asString()); } - return super.visit(node, arg); + addTypeVariableScope(node.getTypeParameters()); + Visitable result = super.visit(node, arg); + typeVariables.removeFirst(); + return result; } @Override @@ -316,9 +324,9 @@ public Visitable visit(ExplicitConstructorInvocationStmt node, Void arg) { public Visitable visit(ForStmt node, Void p) { HashSet currentLocalVariables = new HashSet<>(); localVariables.addFirst(currentLocalVariables); - super.visit(node, p); + Visitable result = super.visit(node, p); localVariables.removeFirst(); - return node; + return result; } @Override @@ -349,54 +357,54 @@ public Visitable visit(ForStmt node, Void p) { public Visitable visit(WhileStmt node, Void p) { HashSet currentLocalVariables = new HashSet<>(); localVariables.addFirst(currentLocalVariables); - super.visit(node, p); + Visitable result = super.visit(node, p); localVariables.removeFirst(); - return node; + return result; } @Override public Visitable visit(SwitchExpr node, Void p) { HashSet currentLocalVariables = new HashSet<>(); localVariables.addFirst(currentLocalVariables); - super.visit(node, p); + Visitable result = super.visit(node, p); localVariables.removeFirst(); - return node; + return result; } @Override public Visitable visit(SwitchEntry node, Void p) { HashSet currentLocalVariables = new HashSet<>(); localVariables.addFirst(currentLocalVariables); - super.visit(node, p); + Visitable result = super.visit(node, p); localVariables.removeFirst(); - return node; + return result; } @Override public Visitable visit(TryStmt node, Void p) { HashSet currentLocalVariables = new HashSet<>(); localVariables.addFirst(currentLocalVariables); - super.visit(node, p); + Visitable result = super.visit(node, p); localVariables.removeFirst(); - return node; + return result; } @Override public Visitable visit(CatchClause node, Void p) { HashSet currentLocalVariables = new HashSet<>(); localVariables.addFirst(currentLocalVariables); - super.visit(node, p); + Visitable result = super.visit(node, p); localVariables.removeFirst(); - return node; + return result; } @Override public Visitable visit(BlockStmt node, Void p) { HashSet currentLocalVariables = new HashSet<>(); localVariables.addFirst(currentLocalVariables); - super.visit(node, p); + Visitable result = super.visit(node, p); localVariables.removeFirst(); - return node; + return result; } @Override @@ -485,6 +493,16 @@ public Visitable visit(FieldDeclaration node, Void arg) { return super.visit(node, arg); } + @Override + public Visitable visit(ConstructorDeclaration node, Void arg) { + // TODO: Loi: do we need to do anything for the parameters, like we do in + // visit(MethodDeclaration)? + addTypeVariableScope(node.getTypeParameters()); + Visitable result = super.visit(node, arg); + typeVariables.removeFirst(); + return result; + } + @Override public Visitable visit(MethodDeclaration node, Void arg) { // a MethodDeclaration instance will have parent node @@ -497,10 +515,16 @@ public Visitable visit(MethodDeclaration node, Void arg) { try { nodeType.resolve(); } catch (UnsolvedSymbolException | UnsupportedOperationException e) { + // TODO: double-check which of these takes precedence if there is a type variable + // with the same simple name as an in-scope class. Which is being referred to in + // a method declaration? It might be the one with smaller scope, which would be... + // difficult...for us to model properly here. if (classAndPackageMap.containsKey(nodeTypeSimpleForm)) { UnsolvedClass syntheticType = new UnsolvedClass(nodeTypeSimpleForm, classAndPackageMap.get(nodeTypeSimpleForm)); this.updateMissingClass(syntheticType); + } else if (this.isTypeVar(nodeTypeSimpleForm)) { + // TODO: Do something here. } else { throw new RuntimeException("Unexpected class: " + nodeTypeSimpleForm); } @@ -523,12 +547,16 @@ public Visitable visit(MethodDeclaration node, Void arg) { updateUnsolvedClassWithMethod(node, nameOfClass, toSimpleName(nodeTypeAsString)); } } - HashSet currentLocalVariables = getParameterFromAMethodDeclaration(node); localVariables.addFirst(currentLocalVariables); - super.visit(node, arg); + // TODO: for some reason the test loops forever if I do this at the beginning of the method?!? + // However, if we don't put this there then the call to isTypeVar above + // won't give the correct answer. + addTypeVariableScope(node.getTypeParameters()); + Visitable result = super.visit(node, arg); localVariables.removeFirst(); - return node; + typeVariables.removeFirst(); + return result; } @Override @@ -1001,6 +1029,37 @@ public boolean isALocalVar(String variableName) { return false; } + /** + * Is the given type name actually an in-scope type variable? + * + * @param typeName a simple name of a type, as written in a source file. The type name might be an + * in-scope type variable. + * @return true iff there is a type variable in scope with this name. Returning false guarantees + * that there is no such type variable, but not that the input is a valid type. + */ + private boolean isTypeVar(String typeName) { + for (Set scope : typeVariables) { + if (scope.contains(typeName)) { + return true; + } + } + return false; + } + + /** + * Adds a scope with the given list of type parameters. Each pair to this method must be paired + * with a call to typeVariables.removeFirst(). + * + * @param typeParameters a list of type parameters + */ + private void addTypeVariableScope(List typeParameters) { + Set typeVariableScope = new HashSet<>(); + for (TypeParameter t : typeParameters) { + typeVariableScope.add(t.getName().asString()); + } + typeVariables.addFirst(typeVariableScope); + } + /** * This method checks if the current run of UnsolvedSymbolVisitor can solve the parameters' types * of a method call From 4cd794b274fe9551735052876a53e629c05205bb Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Tue, 21 Nov 2023 16:05:37 -0500 Subject: [PATCH 15/23] checkpoint, no longer crashing or infinite looping --- .../specimin/TargetMethodFinderVisitor.java | 10 ++++-- .../specimin/UnsolvedSymbolVisitor.java | 35 ++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index ec8f335a..2ade716b 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -199,8 +199,14 @@ public Visitable visit(MethodDeclaration method, Void p) { Type returnType = method.getType(); // JavaParser may misinterpret unresolved array types as reference types. // To ensure accuracy, we resolve the type before proceeding with the check. - if (returnType.resolve() instanceof ResolvedReferenceType) { - usedClass.add(returnType.resolve().asReferenceType().getQualifiedName()); + try { + ResolvedType resolvedType = returnType.resolve(); + if (resolvedType instanceof ResolvedReferenceType) { + usedClass.add(resolvedType.asReferenceType().getQualifiedName()); + } + } catch (UnsupportedOperationException e) { + // Occurs if the type is a type variable, so there is nothing to do. + // The type variable will be resolved later, by the UnsolvedSymbolVisitor. } } Visitable result = super.visit(method, p); diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index eb70dc99..d93aab01 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -425,12 +425,15 @@ public Visitable visit(VariableDeclarator decl, Void p) { } catch (UnsolvedSymbolException | UnsupportedOperationException e) { String typeAsString = declType.asString(); List elements = Splitter.onPattern("\\.").splitToList(typeAsString); - // There could be two cases here: either a fully-qualified class name or a simple class name. + // There could be three cases here: a type variable, a fully-qualified class name, or a simple + // class name. // This is the fully-qualified case. if (elements.size() > 1) { @SuppressWarnings("signature") // since this type is in a fully-qualfied form @FullyQualifiedName String qualifiedTypeName = typeAsString; updateMissingClass(getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName)); + } else if (isTypeVar(typeAsString)) { + // Nothing to do in this case, but we need to skip creating an unsolved class. } /** * Handles the case where the type is a simple class name. Two sub-cases are considered: 1. @@ -508,6 +511,14 @@ public Visitable visit(MethodDeclaration node, Void arg) { // a MethodDeclaration instance will have parent node Node parentNode = node.getParentNode().get(); Type nodeType = node.getType(); + + // TODO: for some reason the test loops forever if I do this at the beginning of the method?!? + // However, if we don't put this there then the call to isTypeVar above + // won't give the correct answer. Hint: running the test for some reason creates a file + // called T.java in the input directory! Suspicion: this is being created and then picked up, + // which leads to the infinite loop. Let's try to figure out why that happens. + addTypeVariableScope(node.getTypeParameters()); + // since this is a return type of a method, it is a dot-separated identifier @SuppressWarnings("signature") @DotSeparatedIdentifiers String nodeTypeAsString = nodeType.asString(); @@ -519,12 +530,12 @@ public Visitable visit(MethodDeclaration node, Void arg) { // with the same simple name as an in-scope class. Which is being referred to in // a method declaration? It might be the one with smaller scope, which would be... // difficult...for us to model properly here. - if (classAndPackageMap.containsKey(nodeTypeSimpleForm)) { + if (this.isTypeVar(nodeTypeSimpleForm)) { + // TODO: Do something here. + } else if (classAndPackageMap.containsKey(nodeTypeSimpleForm)) { UnsolvedClass syntheticType = new UnsolvedClass(nodeTypeSimpleForm, classAndPackageMap.get(nodeTypeSimpleForm)); this.updateMissingClass(syntheticType); - } else if (this.isTypeVar(nodeTypeSimpleForm)) { - // TODO: Do something here. } else { throw new RuntimeException("Unexpected class: " + nodeTypeSimpleForm); } @@ -549,10 +560,6 @@ public Visitable visit(MethodDeclaration node, Void arg) { } HashSet currentLocalVariables = getParameterFromAMethodDeclaration(node); localVariables.addFirst(currentLocalVariables); - // TODO: for some reason the test loops forever if I do this at the beginning of the method?!? - // However, if we don't put this there then the call to isTypeVar above - // won't give the correct answer. - addTypeVariableScope(node.getTypeParameters()); Visitable result = super.visit(node, arg); localVariables.removeFirst(); typeVariables.removeFirst(); @@ -634,7 +641,11 @@ public Visitable visit(ClassOrInterfaceType typeExpr, Void p) { // without any type argument typeRawName = typeRawName.substring(0, typeRawName.indexOf("<")); } - if (isAClassPath(typeRawName)) { + if (isTypeVar(typeRawName)) { + // If the type name itself is an in-scope type variable, just return without attempting + // to create a missing class. + return super.visit(typeExpr, p); + } else if (isAClassPath(typeRawName)) { String packageName = typeRawName.substring(0, typeRawName.lastIndexOf(".")); @SuppressWarnings("signature") // since this is the last element of a class path @ClassGetSimpleName String className = typeRawName.substring(typeRawName.lastIndexOf(".") + 1); @@ -1240,6 +1251,9 @@ public static boolean calledByAnIncompleteSyntheticClass(MethodCallExpr method) * @param missedClass the class to be updated */ public void updateMissingClass(UnsolvedClass missedClass) { + if (missedClass.getClassName().equals("T")) { + throw new RuntimeException("shouldn't be updating T as a missing class!"); + } Iterator iterator = missingClass.iterator(); while (iterator.hasNext()) { UnsolvedClass e = iterator.next(); @@ -1316,6 +1330,9 @@ public void deleteOldSyntheticClass(UnsolvedClass missedClass) { * @param missedClass the class to be added */ public void createMissingClass(UnsolvedClass missedClass) { + if (missedClass.getClassName().equals("T")) { + throw new RuntimeException("probably shouldn't be creating this one"); + } StringBuilder fileContent = new StringBuilder(); fileContent.append(missedClass); String classPackage = missedClass.getPackageName(); From bbc59fb7e43a78950c440649c07e72585a53f2f2 Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Tue, 21 Nov 2023 16:28:58 -0500 Subject: [PATCH 16/23] handle simple type variables --- .../typevarsimple/expected/org/example/ClassWithTypeParam.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/typevarsimple/expected/org/example/ClassWithTypeParam.java b/src/test/resources/typevarsimple/expected/org/example/ClassWithTypeParam.java index 8b17e94a..5408257e 100644 --- a/src/test/resources/typevarsimple/expected/org/example/ClassWithTypeParam.java +++ b/src/test/resources/typevarsimple/expected/org/example/ClassWithTypeParam.java @@ -1,7 +1,7 @@ package org.example; public class ClassWithTypeParam { - T getT() { + public T getT() { throw new Error(); } } From 7ad4b831288301d022198af986b51f6e6986298f Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Tue, 21 Nov 2023 16:37:07 -0500 Subject: [PATCH 17/23] cleanup to prep for review --- .../specimin/UnsolvedSymbolVisitor.java | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index d93aab01..870b0055 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -512,32 +512,27 @@ public Visitable visit(MethodDeclaration node, Void arg) { Node parentNode = node.getParentNode().get(); Type nodeType = node.getType(); - // TODO: for some reason the test loops forever if I do this at the beginning of the method?!? - // However, if we don't put this there then the call to isTypeVar above - // won't give the correct answer. Hint: running the test for some reason creates a file - // called T.java in the input directory! Suspicion: this is being created and then picked up, - // which leads to the infinite loop. Let's try to figure out why that happens. + // This scope logic must happen here, because later in this method there is a check for + // whether the return type is a type variable, which must succeed if the type variable + // was declared for this scope. addTypeVariableScope(node.getTypeParameters()); // since this is a return type of a method, it is a dot-separated identifier @SuppressWarnings("signature") @DotSeparatedIdentifiers String nodeTypeAsString = nodeType.asString(); @ClassGetSimpleName String nodeTypeSimpleForm = toSimpleName(nodeTypeAsString); - try { - nodeType.resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - // TODO: double-check which of these takes precedence if there is a type variable - // with the same simple name as an in-scope class. Which is being referred to in - // a method declaration? It might be the one with smaller scope, which would be... - // difficult...for us to model properly here. - if (this.isTypeVar(nodeTypeSimpleForm)) { - // TODO: Do something here. - } else if (classAndPackageMap.containsKey(nodeTypeSimpleForm)) { - UnsolvedClass syntheticType = - new UnsolvedClass(nodeTypeSimpleForm, classAndPackageMap.get(nodeTypeSimpleForm)); - this.updateMissingClass(syntheticType); - } else { - throw new RuntimeException("Unexpected class: " + nodeTypeSimpleForm); + if (!this.isTypeVar(nodeTypeSimpleForm)) { + // Don't attempt to resolve a type variable, since we will inevitably fail. + try { + nodeType.resolve(); + } catch (UnsolvedSymbolException | UnsupportedOperationException e) { + if (classAndPackageMap.containsKey(nodeTypeSimpleForm)) { + UnsolvedClass syntheticType = + new UnsolvedClass(nodeTypeSimpleForm, classAndPackageMap.get(nodeTypeSimpleForm)); + this.updateMissingClass(syntheticType); + } else { + throw new RuntimeException("Unexpected class: " + nodeTypeSimpleForm); + } } } From 0254a20262755677b40e9422edc4ab77c3da2b25 Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Tue, 21 Nov 2023 16:38:31 -0500 Subject: [PATCH 18/23] remove debug code --- .../checkerframework/specimin/UnsolvedSymbolVisitor.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 870b0055..8d8becca 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -1246,9 +1246,6 @@ public static boolean calledByAnIncompleteSyntheticClass(MethodCallExpr method) * @param missedClass the class to be updated */ public void updateMissingClass(UnsolvedClass missedClass) { - if (missedClass.getClassName().equals("T")) { - throw new RuntimeException("shouldn't be updating T as a missing class!"); - } Iterator iterator = missingClass.iterator(); while (iterator.hasNext()) { UnsolvedClass e = iterator.next(); @@ -1325,9 +1322,6 @@ public void deleteOldSyntheticClass(UnsolvedClass missedClass) { * @param missedClass the class to be added */ public void createMissingClass(UnsolvedClass missedClass) { - if (missedClass.getClassName().equals("T")) { - throw new RuntimeException("probably shouldn't be creating this one"); - } StringBuilder fileContent = new StringBuilder(); fileContent.append(missedClass); String classPackage = missedClass.getPackageName(); From 89d47ff93f4d08336dbbcbbfc79efa253ca2d804 Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Wed, 22 Nov 2023 14:49:06 -0500 Subject: [PATCH 19/23] failing test --- .../specimin/TypeVarCollisionTest.java | 13 +++++++++++++ .../typevar-collision/expected/org/Foo.java | 7 +++++++ .../resources/typevar-collision/input/com/T.java | 3 +++ .../resources/typevar-collision/input/org/Foo.java | 10 ++++++++++ 4 files changed, 33 insertions(+) create mode 100644 src/test/java/org/checkerframework/specimin/TypeVarCollisionTest.java create mode 100644 src/test/resources/typevar-collision/expected/org/Foo.java create mode 100644 src/test/resources/typevar-collision/input/com/T.java create mode 100644 src/test/resources/typevar-collision/input/org/Foo.java diff --git a/src/test/java/org/checkerframework/specimin/TypeVarCollisionTest.java b/src/test/java/org/checkerframework/specimin/TypeVarCollisionTest.java new file mode 100644 index 00000000..5953e68d --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/TypeVarCollisionTest.java @@ -0,0 +1,13 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** This test checks if Specimin can work for methods that have type parameters themselves. */ +public class TypeVarCollisionTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "typevar-collision", new String[] {"org/Foo.java"}, new String[] {"org.Foo#useT(T)"}); + } +} diff --git a/src/test/resources/typevar-collision/expected/org/Foo.java b/src/test/resources/typevar-collision/expected/org/Foo.java new file mode 100644 index 00000000..0474d235 --- /dev/null +++ b/src/test/resources/typevar-collision/expected/org/Foo.java @@ -0,0 +1,7 @@ +package org; + +public class Foo { + T useT(T t) { + return t; + } +} diff --git a/src/test/resources/typevar-collision/input/com/T.java b/src/test/resources/typevar-collision/input/com/T.java new file mode 100644 index 00000000..23b63d35 --- /dev/null +++ b/src/test/resources/typevar-collision/input/com/T.java @@ -0,0 +1,3 @@ +package com; + +public class T {} \ No newline at end of file diff --git a/src/test/resources/typevar-collision/input/org/Foo.java b/src/test/resources/typevar-collision/input/org/Foo.java new file mode 100644 index 00000000..fad1e99b --- /dev/null +++ b/src/test/resources/typevar-collision/input/org/Foo.java @@ -0,0 +1,10 @@ +package org; + +// This import isn't used. +import com.T; + +public class Foo { + T useT(T t) { + return t; + } +} From 2ffc4d63225561d99934b3ec27ef3fa4f61a534e Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Wed, 22 Nov 2023 15:03:06 -0500 Subject: [PATCH 20/23] address CR comments --- .../specimin/TargetMethodFinderVisitor.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index 2ade716b..07594473 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -205,8 +205,10 @@ public Visitable visit(MethodDeclaration method, Void p) { usedClass.add(resolvedType.asReferenceType().getQualifiedName()); } } catch (UnsupportedOperationException e) { - // Occurs if the type is a type variable, so there is nothing to do. - // The type variable will be resolved later, by the UnsolvedSymbolVisitor. + // Occurs if the type is a type variable, so there is nothing to do: + // 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. } } Visitable result = super.visit(method, p); From 5acffa51ef9a925f38be8d47596d329f64320150 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Nov 2023 11:42:53 -0500 Subject: [PATCH 21/23] remove annotations --- .../java/org/checkerframework/specimin/SpeciminRunner.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java index 49941560..27de3358 100644 --- a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java +++ b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java @@ -174,8 +174,7 @@ public static void performMinimization( JavaTypeCorrect typeCorrecter = new JavaTypeCorrect(root, relatedClass, filesAndAssociatedTypes); typeCorrecter.correctTypesForAllFiles(); - Map<@ClassGetSimpleName String, @ClassGetSimpleName String> typesToChange = - typeCorrecter.getTypeToChange(); + Map typesToChange = typeCorrecter.getTypeToChange(); addMissingClass.updateTypes(typesToChange); for (String directory : relatedClass) { From 0ea0c7c73cf485375a875de09fd6db753ad08db3 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Nov 2023 12:06:23 -0500 Subject: [PATCH 22/23] add exception for type variables --- .../org/checkerframework/specimin/GetTypesFullNameVisitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/checkerframework/specimin/GetTypesFullNameVisitor.java b/src/main/java/org/checkerframework/specimin/GetTypesFullNameVisitor.java index 19e083e4..bd02d46b 100644 --- a/src/main/java/org/checkerframework/specimin/GetTypesFullNameVisitor.java +++ b/src/main/java/org/checkerframework/specimin/GetTypesFullNameVisitor.java @@ -50,7 +50,7 @@ public Visitable visit(ClassOrInterfaceType type, Void p) { String typeFullName; try { typeFullName = type.resolve().getQualifiedName(); - } catch (UnsolvedSymbolException e) { + } catch (UnsolvedSymbolException | UnsupportedOperationException e) { return super.visit(type, p); } if (fileAndAssociatedTypes.containsKey(fileDirectory)) { From 78e201f99af545ed709cb9539237a2267c992de5 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Nov 2023 12:13:26 -0500 Subject: [PATCH 23/23] restore order of expected file in AnonymousClass --- .../anonymousclass/expected/com/nameless/SomeClass.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java b/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java index 895a5721..5683d299 100644 --- a/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java +++ b/src/test/resources/anonymousclass/expected/com/nameless/SomeClass.java @@ -2,11 +2,10 @@ public class SomeClass { - public int getLocalVar() { + public SomeClass() { throw new Error(); } - - public SomeClass() { + public int getLocalVar() { throw new Error(); } }