diff --git a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java index 24558146..79d87012 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMethodFinderVisitor.java @@ -12,8 +12,11 @@ import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.expr.SuperExpr; +import com.github.javaparser.ast.stmt.CatchClause; import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt; +import com.github.javaparser.ast.type.ReferenceType; import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.type.UnionType; import com.github.javaparser.ast.visitor.ModifierVisitor; import com.github.javaparser.ast.visitor.Visitable; import com.github.javaparser.resolution.UnsolvedSymbolException; @@ -220,19 +223,34 @@ public Visitable visit(MethodDeclaration method, Void p) { @Override public Visitable visit(Parameter para, Void p) { if (insideTargetMethod) { - ResolvedType paraType = para.resolve().getType(); - if (paraType.isReferenceType()) { - String paraTypeFullName = - paraType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); - usedClass.add(paraTypeFullName); - for (ResolvedType typeParameterValue : paraType.asReferenceType().typeParametersValues()) { - String typeParameterValueName = typeParameterValue.describe(); - if (typeParameterValueName.contains("<")) { - // removing the "<...>" part if there is any. - typeParameterValueName = - typeParameterValueName.substring(0, typeParameterValueName.indexOf("<")); + Type type = para.getType(); + if (type.isUnionType()) { + resolveUnionType(type.asUnionType()); + } else { + // Parameter resolution (para.resolve()) does not work in catch clause. + // However, resolution works on the type of the parameter. + // Bug report: https://github.com/javaparser/javaparser/issues/4240 + ResolvedType paramType; + if (para.getParentNode().isPresent() && para.getParentNode().get() instanceof CatchClause) { + paramType = para.getType().resolve(); + } else { + paramType = para.resolve().getType(); + } + + if (paramType.isReferenceType()) { + String paraTypeFullName = + paramType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); + usedClass.add(paraTypeFullName); + for (ResolvedType typeParameterValue : + paramType.asReferenceType().typeParametersValues()) { + String typeParameterValueName = typeParameterValue.describe(); + if (typeParameterValueName.contains("<")) { + // removing the "<...>" part if there is any. + typeParameterValueName = + typeParameterValueName.substring(0, typeParameterValueName.indexOf("<")); + } + usedClass.add(typeParameterValueName); } - usedClass.add(typeParameterValueName); } } } @@ -310,4 +328,18 @@ public Visitable visit(NameExpr expr, Void p) { } return super.visit(expr, p); } + + /** + * Resolves unionType parameters one by one and adds them in the usedClass set. + * + * @param type unionType parameter + */ + private void resolveUnionType(UnionType type) { + for (ReferenceType param : type.getElements()) { + ResolvedType paramType = param.resolve(); + String paraTypeFullName = + paramType.asReferenceType().getTypeDeclaration().get().getQualifiedName(); + usedClass.add(paraTypeFullName); + } + } } diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedClass.java b/src/main/java/org/checkerframework/specimin/UnsolvedClass.java index e29b63a1..e9df67ea 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedClass.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedClass.java @@ -37,6 +37,9 @@ public class UnsolvedClass { /** This field records the number of type variables for this class */ private int numberOfTypeVariables = 0; + /** This field records if the class is a custom exception */ + private boolean isExceptionType = false; + /** * Create an instance of UnsolvedClass * @@ -44,10 +47,23 @@ public class UnsolvedClass { * @param packageName the name of the package */ public UnsolvedClass(@ClassGetSimpleName String className, String packageName) { + this(className, packageName, false); + } + + /** + * Create an instance of UnsolvedClass + * + * @param className the name of the class + * @param packageName the name of the package + * @param isException does the class represents an exception? + */ + public UnsolvedClass( + @ClassGetSimpleName String className, String packageName, boolean isException) { this.className = className; this.methods = new LinkedHashSet<>(); this.packageName = packageName; this.classFields = new LinkedHashSet<>(); + this.isExceptionType = isException; } /** @@ -105,7 +121,11 @@ public void addFields(String variableExpression) { this.classFields.add(variableExpression); } - /** This method sets the number of type variables for the current class */ + /** + * This method sets the number of type variables for the current class + * + * @param numberOfTypeVariables number of type variable in this class. + */ public void setNumberOfTypeVariables(int numberOfTypeVariables) { this.numberOfTypeVariables = numberOfTypeVariables; } @@ -172,7 +192,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(getTypeVariablesAsString()).append(" {\n"); + sb.append("public class ").append(className).append(getTypeVariablesAsString()); + if (isExceptionType) { + sb.append(" extends Exception"); + } + sb.append(" {\n"); for (String variableDeclarations : classFields) { sb.append(" " + "public " + variableDeclarations + ";\n"); } diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java index 64cef010..de903af8 100644 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java @@ -31,6 +31,7 @@ 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.type.UnionType; import com.github.javaparser.ast.visitor.ModifierVisitor; import com.github.javaparser.ast.visitor.Visitable; import com.github.javaparser.resolution.UnsolvedSymbolException; @@ -57,6 +58,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetSimpleName; import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; @@ -526,7 +528,7 @@ public Visitable visit(MethodDeclaration node, Void arg) { try { nodeType.resolve(); } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - updateUnsolvedClassWithClassName(nodeTypeSimpleForm); + updateUnsolvedClassWithClassName(nodeTypeSimpleForm, false); } } @@ -655,24 +657,22 @@ public Visitable visit(ClassOrInterfaceType typeExpr, Void p) { @Override public Visitable visit(Parameter parameter, Void p) { try { - parameter.resolve(); + if (parameter.getType() instanceof UnionType) { + resolveUnionType(parameter); + } else { + if (parameter.getParentNode().isPresent() + && parameter.getParentNode().get() instanceof CatchClause) { + parameter.getType().resolve(); + } else { + 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. catch (UnsolvedSymbolException | UnsupportedOperationException e) { - String parameterInString = parameter.toString(); - if (isAClassPath(parameterInString)) { - // parameterInString needs to be a fully-qualified name. As this parameter has a form of - // class path, we can say that it is a fully-qualified name - @SuppressWarnings("signature") - UnsolvedClass newClass = getSimpleSyntheticClassFromFullyQualifiedName(parameterInString); - updateMissingClass(newClass); - } else { - // since it is unsolved, it could not be a primitive type - @ClassGetSimpleName String className = parameter.getType().asClassOrInterfaceType().getName().asString(); - updateUnsolvedClassWithClassName(className); - } + handleParameterResolveFailure(parameter); } gotException = true; return super.visit(parameter, p); @@ -695,7 +695,7 @@ public Visitable visit(ObjectCreationExpr newExpr, Void p) { try { List argumentsCreation = getArgumentsFromObjectCreation(newExpr); UnsolvedMethod creationMethod = new UnsolvedMethod("", type, argumentsCreation); - updateUnsolvedClassWithClassName(type, creationMethod); + updateUnsolvedClassWithClassName(type, false, creationMethod); } catch (Exception q) { // can not solve the parameters for this object creation in this current run } @@ -705,6 +705,46 @@ public Visitable visit(ObjectCreationExpr newExpr, Void p) { return newExpr; } + /** + * @param parameter parameter from visitor method which is unsolvable. + */ + private void handleParameterResolveFailure(@NonNull Parameter parameter) { + String parameterInString = parameter.toString(); + if (isAClassPath(parameterInString)) { + // parameterInString needs to be a fully-qualified name. As this parameter has a form of + // class path, we can say that it is a fully-qualified name + @SuppressWarnings("signature") + UnsolvedClass newClass = getSimpleSyntheticClassFromFullyQualifiedName(parameterInString); + updateMissingClass(newClass); + } else { + // since it is unsolved, it could not be a primitive type + @ClassGetSimpleName String className = parameter.getType().asClassOrInterfaceType().getName().asString(); + if (parameter.getParentNode().isPresent() + && parameter.getParentNode().get() instanceof CatchClause) { + updateUnsolvedClassWithClassName(className, true); + } else { + updateUnsolvedClassWithClassName(className, false); + } + } + } + + /** + * Given the unionType parameter, this method will try resolving each element separately. If any + * of the element is unsolvable, an unsolved class instance will be created to generate synthetic + * class for the element. + * + * @param parameter unionType parameter from visitor class + */ + private void resolveUnionType(@NonNull Parameter parameter) { + for (var param : parameter.getType().asUnionType().getElements()) { + try { + param.resolve(); + } catch (UnsolvedSymbolException | UnsupportedOperationException e) { + handleParameterResolveFailure(parameter); + } + } + } + /** * Given the variable type and the basic declaration of that variable (such as "int x", "boolean * y", "Car redTruck",...), this methods will add an initial value to that declaration of the @@ -790,7 +830,7 @@ public void updateUnsolvedClassWithMethod( returnType = desiredReturnType; } UnsolvedMethod thisMethod = new UnsolvedMethod(methodName, returnType, listOfParameters); - UnsolvedClass missingClass = updateUnsolvedClassWithClassName(className, thisMethod); + UnsolvedClass missingClass = updateUnsolvedClassWithClassName(className, false, thisMethod); syntheticMethodAndClass.put(methodName, missingClass); // if the return type is not specified, a synthetic return type will be created. This part of @@ -873,15 +913,18 @@ public void updateClassesFromJarSourcesForMethodCall(MethodCallExpr expr) { * @param nameOfClass the name of an unsolved class * @param unsolvedMethods unsolved methods to add to the class before updating this visitor's set * missing classes (optional, may be omitted) + * @param isExceptionType if the class is of exceptionType * @return the newly-created UnsolvedClass method, for further processing. This output may be * ignored. */ public UnsolvedClass updateUnsolvedClassWithClassName( - @ClassGetSimpleName String nameOfClass, UnsolvedMethod... unsolvedMethods) { + @ClassGetSimpleName String nameOfClass, + boolean isExceptionType, + UnsolvedMethod... unsolvedMethods) { // if the name of the class is not present among import statements, we assume that this unsolved // class is in the same directory as the current class String packageName = classAndPackageMap.getOrDefault(nameOfClass, currentPackage); - UnsolvedClass result = new UnsolvedClass(nameOfClass, packageName); + UnsolvedClass result = new UnsolvedClass(nameOfClass, packageName, isExceptionType); for (UnsolvedMethod unsolvedMethod : unsolvedMethods) { result.addMethod(unsolvedMethod); } diff --git a/src/test/java/org/checkerframework/specimin/CustomExceptionTest.java b/src/test/java/org/checkerframework/specimin/CustomExceptionTest.java new file mode 100644 index 00000000..558c2368 --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/CustomExceptionTest.java @@ -0,0 +1,14 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +public class CustomExceptionTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "customexception", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#test()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/Issue38Test.java b/src/test/java/org/checkerframework/specimin/Issue38Test.java new file mode 100644 index 00000000..ecbed004 --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/Issue38Test.java @@ -0,0 +1,15 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** This test checks if Specimin will work for Union types */ +public class Issue38Test { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "issue38", + new String[] {"com/example/Issue38.java"}, + new String[] {"com.example.Issue38#test()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/NestedCatchClauseTest.java b/src/test/java/org/checkerframework/specimin/NestedCatchClauseTest.java new file mode 100644 index 00000000..b1dec9fe --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/NestedCatchClauseTest.java @@ -0,0 +1,14 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +public class NestedCatchClauseTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "nestedcatchclause", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#test()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/UnsolvableExceptionTest.java b/src/test/java/org/checkerframework/specimin/UnsolvableExceptionTest.java new file mode 100644 index 00000000..bc3a6cfe --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/UnsolvableExceptionTest.java @@ -0,0 +1,14 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +public class UnsolvableExceptionTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "unsolvedexception", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#test()"}); + } +} diff --git a/src/test/resources/customexception/expected/com/example/CustomException.java b/src/test/resources/customexception/expected/com/example/CustomException.java new file mode 100644 index 00000000..c3625957 --- /dev/null +++ b/src/test/resources/customexception/expected/com/example/CustomException.java @@ -0,0 +1,8 @@ +package com.example; + +public class CustomException extends Exception { + + public CustomException(String msg) { + throw new Error(); + } +} diff --git a/src/test/resources/customexception/expected/com/example/Simple.java b/src/test/resources/customexception/expected/com/example/Simple.java new file mode 100644 index 00000000..218d41e0 --- /dev/null +++ b/src/test/resources/customexception/expected/com/example/Simple.java @@ -0,0 +1,12 @@ +package com.example; + +public class Simple { + + public void test() { + try { + throw new CustomException("dummy"); + } catch (CustomException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/resources/customexception/input/com/example/CustomException.java b/src/test/resources/customexception/input/com/example/CustomException.java new file mode 100644 index 00000000..a0fcf0e3 --- /dev/null +++ b/src/test/resources/customexception/input/com/example/CustomException.java @@ -0,0 +1,5 @@ +package com.example; +public class CustomException extends Exception { + public CustomException (String msg) { + } +} diff --git a/src/test/resources/customexception/input/com/example/Simple.java b/src/test/resources/customexception/input/com/example/Simple.java new file mode 100644 index 00000000..db4b6f40 --- /dev/null +++ b/src/test/resources/customexception/input/com/example/Simple.java @@ -0,0 +1,14 @@ +package com.example; + +import org.checkerframework.specimin.CustomExceptionTest; + +public class Simple { + + public void test() { + try { + throw new CustomException("dummy"); + } catch (CustomException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/resources/issue38/expected/com/example/Issue38.java b/src/test/resources/issue38/expected/com/example/Issue38.java new file mode 100644 index 00000000..edd3b42a --- /dev/null +++ b/src/test/resources/issue38/expected/com/example/Issue38.java @@ -0,0 +1,16 @@ +package com.example; + +public class Issue38 { + + static class ExampleClass { + } + + public void test() { + try { + Class exampleClass = ExampleClass.class; + ExampleClass instance = (ExampleClass) exampleClass.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/resources/issue38/input/com/example/Issue38.java b/src/test/resources/issue38/input/com/example/Issue38.java new file mode 100644 index 00000000..9b4b2592 --- /dev/null +++ b/src/test/resources/issue38/input/com/example/Issue38.java @@ -0,0 +1,20 @@ +package com.example; + +public class Issue38 { + + static class ExampleClass { + public ExampleClass(int value) { + // Constructor with a parameter + } + } + + public void test() { + try { + // Attempting to create an instance of ExampleClass, which has no default constructor + Class exampleClass = ExampleClass.class; + ExampleClass instance = (ExampleClass) exampleClass.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/resources/nestedcatchclause/expected/com/example/CustomException.java b/src/test/resources/nestedcatchclause/expected/com/example/CustomException.java new file mode 100644 index 00000000..c3625957 --- /dev/null +++ b/src/test/resources/nestedcatchclause/expected/com/example/CustomException.java @@ -0,0 +1,8 @@ +package com.example; + +public class CustomException extends Exception { + + public CustomException(String msg) { + throw new Error(); + } +} diff --git a/src/test/resources/nestedcatchclause/expected/com/example/Simple.java b/src/test/resources/nestedcatchclause/expected/com/example/Simple.java new file mode 100644 index 00000000..8c3fa9b7 --- /dev/null +++ b/src/test/resources/nestedcatchclause/expected/com/example/Simple.java @@ -0,0 +1,20 @@ +package com.example; + +public class Simple { + + public void test() { + try { + throw new CustomException("dummy"); + } catch (CustomException e) { + System.out.println("Caught custom exception: " + e.getMessage()); + if (e.getMessage().contains("occurred")) { + System.out.println("Exception message indicates an occurrence."); + } + handleException(e); + } + } + + private void handleException(CustomException e) { + throw new Error(); + } +} \ No newline at end of file diff --git a/src/test/resources/nestedcatchclause/input/com/example/CustomException.java b/src/test/resources/nestedcatchclause/input/com/example/CustomException.java new file mode 100644 index 00000000..a0fcf0e3 --- /dev/null +++ b/src/test/resources/nestedcatchclause/input/com/example/CustomException.java @@ -0,0 +1,5 @@ +package com.example; +public class CustomException extends Exception { + public CustomException (String msg) { + } +} diff --git a/src/test/resources/nestedcatchclause/input/com/example/Simple.java b/src/test/resources/nestedcatchclause/input/com/example/Simple.java new file mode 100644 index 00000000..5ba101d2 --- /dev/null +++ b/src/test/resources/nestedcatchclause/input/com/example/Simple.java @@ -0,0 +1,21 @@ +package com.example; + +import org.checkerframework.specimin.CustomExceptionTest; + +public class Simple { + + public void test() { + try { + throw new CustomException("dummy"); + } catch (CustomException e) { + System.out.println("Caught custom exception: " + e.getMessage()); + if (e.getMessage().contains("occurred")) { + System.out.println("Exception message indicates an occurrence."); + } + handleException(e); + } + } + private void handleException(CustomException e) { + System.out.println("Handling the exception: " + e.getMessage()); + } +} diff --git a/src/test/resources/unsolvedexception/expected/com/example/CustomException.java b/src/test/resources/unsolvedexception/expected/com/example/CustomException.java new file mode 100644 index 00000000..2cc3692c --- /dev/null +++ b/src/test/resources/unsolvedexception/expected/com/example/CustomException.java @@ -0,0 +1,8 @@ +package com.example; + +public class CustomException extends Exception { + + public CustomException(java.lang.String parameter0) { + throw new Error(); + } +} \ No newline at end of file diff --git a/src/test/resources/unsolvedexception/expected/com/example/Simple.java b/src/test/resources/unsolvedexception/expected/com/example/Simple.java new file mode 100644 index 00000000..8131e25f --- /dev/null +++ b/src/test/resources/unsolvedexception/expected/com/example/Simple.java @@ -0,0 +1,12 @@ +package com.example; + +public class Simple { + + public void test() { + try { + throw new CustomException("It's an unsolvable custom exception"); + } catch (CustomException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/resources/unsolvedexception/input/com/example/Simple.java b/src/test/resources/unsolvedexception/input/com/example/Simple.java new file mode 100644 index 00000000..46e51ef4 --- /dev/null +++ b/src/test/resources/unsolvedexception/input/com/example/Simple.java @@ -0,0 +1,13 @@ +package com.example; + +import org.checkerframework.specimin.CustomExceptionTest; + +public class Simple { + public void test() { + try { + throw new CustomException("It's an unsolvable custom exception"); + } catch (CustomException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file