From 23b4bd9531c2fc37833bc60592647384ce4c15e3 Mon Sep 17 00:00:00 2001 From: Alberto Goffi Date: Tue, 27 Feb 2018 22:09:41 +0100 Subject: [PATCH] Add support for enum types (#205) * Improve code fortmatting for better readability * Add an initial support for enum types * Add test for enum support * Reformat according to code style --- .../toradocu/extractor/JavadocExtractor.java | 72 +++++++++--- .../extractor/JavadocExtractorOnEnumTest.java | 111 ++++++++++++++++++ src/test/resources/example/AnEnum.java | 13 ++ 3 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 src/test/java/org/toradocu/extractor/JavadocExtractorOnEnumTest.java create mode 100644 src/test/resources/example/AnEnum.java diff --git a/src/main/java/org/toradocu/extractor/JavadocExtractor.java b/src/main/java/org/toradocu/extractor/JavadocExtractor.java index 2c239ebe..18f61d5f 100644 --- a/src/main/java/org/toradocu/extractor/JavadocExtractor.java +++ b/src/main/java/org/toradocu/extractor/JavadocExtractor.java @@ -8,10 +8,8 @@ import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.body.BodyDeclaration; -import com.github.javaparser.ast.body.CallableDeclaration; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.*; +import com.github.javaparser.ast.nodeTypes.NodeWithConstructors; import com.github.javaparser.ast.nodeTypes.modifiers.NodeWithPrivateModifier; import com.github.javaparser.javadoc.Javadoc; import com.github.javaparser.javadoc.JavadocBlockTag; @@ -403,27 +401,31 @@ private List getExecutables(Class clazz) { public List> getExecutables(String className, String sourcePath) throws FileNotFoundException { final List> sourceExecutables = new ArrayList<>(); - final ClassOrInterfaceDeclaration sourceClass = getClassDefinition(className, sourcePath); - sourceExecutables.addAll(sourceClass.getConstructors()); - sourceExecutables.addAll(sourceClass.getMethods()); + final NodeWithConstructors target = getTypeDefinition(className, sourcePath); + sourceExecutables.addAll(target.getConstructors()); + sourceExecutables.addAll(target.getMethods()); sourceExecutables.removeIf(NodeWithPrivateModifier::isPrivate); // Ignore private members. return Collections.unmodifiableList(sourceExecutables); } - private ClassOrInterfaceDeclaration getClassDefinition(String className, String sourcePath) + private NodeWithConstructors getTypeDefinition(String typeName, String sourcePath) throws FileNotFoundException { final CompilationUnit cu = JavaParser.parse(new File(sourcePath)); String nestedClassName = ""; - if (className.contains("$")) { + int dollarsPosition = typeName.indexOf("$"); + if (dollarsPosition != -1) { // Nested class. - nestedClassName = className.substring(className.indexOf("$") + 1, className.length()); - className = className.substring(0, className.indexOf("$")); + nestedClassName = typeName.substring(dollarsPosition + 1, typeName.length()); + typeName = typeName.substring(0, dollarsPosition); } - Optional definitionOpt = cu.getClassByName(className); + Optional> definitionOpt = cu.getClassByName(typeName); if (!definitionOpt.isPresent()) { - definitionOpt = cu.getInterfaceByName(className); + definitionOpt = cu.getInterfaceByName(typeName); + } + if (!definitionOpt.isPresent()) { + definitionOpt = cu.getEnumByName(typeName); } if (definitionOpt.isPresent()) { @@ -440,12 +442,13 @@ private ClassOrInterfaceDeclaration getClassDefinition(String className, String } } } else { - // Top-level class or interface. + // Top-level class, enum, or interface. return definitionOpt.get(); } } + throw new IllegalArgumentException( - "Impossible to find a class or interface with name " + className + " in " + sourcePath); + "Impossible to find a class or interface with name " + typeName + " in " + sourcePath); } /** @@ -462,6 +465,7 @@ private Map> mapExecutables( String className) { filterOutGeneratedConstructors(reflectionExecutables, sourceExecutables, className); + filterOutEnumMethods(reflectionExecutables, sourceExecutables); if (reflectionExecutables.size() != sourceExecutables.size()) { // TODO Add the differences to the error message to better characterize the error. @@ -494,6 +498,29 @@ && sameParamTypes(e.getParameters(), sourceCallable.getParameters())) return map; } + private void filterOutEnumMethods( + List reflectionExecutables, List> sourceExecutables) { + final List sourceExecutableNames = + sourceExecutables.stream().map(it -> it.getName().asString()).collect(toList()); + // Remove values() method. + reflectionExecutables.removeIf( + it -> { + final String executableName = it.getName(); + return executableName.equals("values") + && !sourceExecutableNames.contains(executableName) + && it.getParameterCount() == 0; + }); + // Remove valueOf(java.lang.String) method. + reflectionExecutables.removeIf( + it -> { + final String executableName = it.getName(); + return executableName.equals("valueOf") + && !sourceExecutableNames.contains(executableName) + && it.getParameterCount() == 1 + && it.getParameters()[0].getType().getName().equals("java.lang.String"); + }); + } + /** * Removes compiler-generated constructors from {@code reflectionExecutables}. * @@ -661,23 +688,28 @@ private Class findExceptionType( String exceptionTypeName, String className) throws ClassNotFoundException { + try { return Reflection.getClass(exceptionTypeName); } catch (ClassNotFoundException e) { // Intentionally empty: Apply other heuristics to load the exception type. } - // Try to load a nested class. + + // Try to load the exception class from java.lang package. try { - return Reflection.getClass(className + "$" + exceptionTypeName); + return Reflection.getClass("java.lang." + exceptionTypeName); } catch (ClassNotFoundException e) { // Intentionally empty: Apply other heuristics to load the exception type. } + + // Try to load a nested class. try { - return Reflection.getClass("java.lang." + exceptionTypeName); + return Reflection.getClass(className + "$" + exceptionTypeName); } catch (ClassNotFoundException e) { // Intentionally empty: Apply other heuristics to load the exception type. } - // Look in classes of package. + + // Look in classes of the target class' package. for (String classInPackage : classesInPackage) { if (classInPackage.contains(exceptionTypeName)) { // TODO Add a comment explaining why the following check is needed. @@ -687,6 +719,7 @@ private Class findExceptionType( return Reflection.getClass(classInPackage); } } + // Look for an import statement to complete exception type name. CompilationUnit cu = getCompilationUnit(sourceCallable); final NodeList imports = cu.getImports(); @@ -702,6 +735,7 @@ private Class findExceptionType( // Intentionally empty: Apply other heuristics to load the exception type. } } + // TODO Improve error message. throw new ClassNotFoundException( "Unable to load exception type " + exceptionTypeName + ". Is it on the classpath?"); diff --git a/src/test/java/org/toradocu/extractor/JavadocExtractorOnEnumTest.java b/src/test/java/org/toradocu/extractor/JavadocExtractorOnEnumTest.java new file mode 100644 index 00000000..5778a9dd --- /dev/null +++ b/src/test/java/org/toradocu/extractor/JavadocExtractorOnEnumTest.java @@ -0,0 +1,111 @@ +package org.toradocu.extractor; + +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import org.junit.BeforeClass; +import org.junit.Test; +import org.toradocu.conf.ClassDirsConverter; +import org.toradocu.conf.Configuration; +import org.toradocu.testlib.ToradocuJavaCompiler; +import org.toradocu.util.Reflection; + +/** + * Tests {@code JavadocExtractor} on the example class example.AClass in src/test/resources/example. + */ +public class JavadocExtractorOnEnumTest { + + private static final String EXAMPLE_SRC = "src/test/resources"; + private static final String TARGET_CLASS = "example.AnEnum"; + private static DocumentedType documentedType; + private static List members; + + @BeforeClass + public static void setUp() throws IOException, ClassNotFoundException { + compileSources(); + documentedType = runJavadocExtractor(); + members = documentedType.getDocumentedExecutables(); + } + + @Test + public void classUnderAnalysis() throws ClassNotFoundException { + final Class targetClass = Reflection.getClass(TARGET_CLASS); + final String docTypeClassName = documentedType.getDocumentedClass().getName(); + final String targetClassName = targetClass.getName(); + assertThat(docTypeClassName, is(equalTo(targetClassName))); + } + + @Test + public void numberOfExecutableMembers() { + assertThat(members.size(), is(1)); + } + + @Test + public void methodIsEven() { + DocumentedExecutable member = members.get(0); + assertThat(member.isConstructor(), is(false)); + + final List parameters = member.getParameters(); + assertThat(parameters.size(), is(1)); + final DocumentedParameter parameter = parameters.get(0); + assertThat(parameter.getName(), is("num")); + assertThat(parameter.getType(), is(Integer.class)); + assertThat(parameter.isNullable(), is(nullValue())); + + final List paramTags = member.paramTags(); + assertThat(paramTags.size(), is(1)); + final ParamTag paramTag = paramTags.get(0); + assertThat(paramTag.getParameter(), is(equalTo(parameter))); + assertThat(paramTag.getComment().getText(), is("must not be null")); + + final ReturnTag returnTag = member.returnTag(); + assertThat(returnTag.getComment().getText(), is("true if num is even, false otherwise")); + + final List throwsTags = member.throwsTags(); + assertThat(throwsTags, is(empty())); + + assertThat(member.getName(), is("isEven")); + assertThat(member.getSignature(), is("isEven(java.lang.Integer num)")); + assertThat(member.toString(), is("public boolean example.AnEnum.isEven(java.lang.Integer)")); + assertThat(member.getDeclaringClass().getName(), is("example.AnEnum")); + } + + private static DocumentedType runJavadocExtractor() + throws ClassNotFoundException, FileNotFoundException, MalformedURLException { + final URL url = Paths.get(EXAMPLE_SRC).toUri().toURL(); + Configuration.INSTANCE.classDirs = Collections.singletonList(url); + final JavadocExtractor javadocExtractor = new JavadocExtractor(); + return javadocExtractor.extract(TARGET_CLASS, EXAMPLE_SRC); + } + + private static void compileSources() throws IOException { + final String examplePath = EXAMPLE_SRC + "/example"; + final File sourceDir = new File(examplePath); + List sourceFiles = + Files.walk(sourceDir.toPath()) + .filter(p -> p.getFileName().toString().endsWith(".java")) + .map(Path::toString) + .collect(toList()); + if (sourceFiles.isEmpty()) { + fail("No Java files to compile found in " + sourceDir); + } + boolean compilationOK = ToradocuJavaCompiler.run(sourceFiles); + if (!compilationOK) { + fail("Error(s) during compilation of test source files."); + } + Configuration.INSTANCE.classDirs = new ClassDirsConverter().convert(examplePath); + } +} diff --git a/src/test/resources/example/AnEnum.java b/src/test/resources/example/AnEnum.java new file mode 100644 index 00000000..7a9872e2 --- /dev/null +++ b/src/test/resources/example/AnEnum.java @@ -0,0 +1,13 @@ +package example; + +public enum AnEnum { + FOO, BAR, BAZ; + + /** + * @param num must not be null + * @return true if num is even, false otherwise + */ + public boolean isEven(Integer num) { + return num % 2 == 0; + } +}