Skip to content

Commit

Permalink
Add support for enum types (#205)
Browse files Browse the repository at this point in the history
* Improve code fortmatting for better readability

* Add an initial support for enum types

* Add test for enum support

* Reformat according to code style
  • Loading branch information
albertogoffi authored Feb 27, 2018
1 parent 33a6073 commit 23b4bd9
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 19 deletions.
72 changes: 53 additions & 19 deletions src/main/java/org/toradocu/extractor/JavadocExtractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -403,27 +401,31 @@ private List<Executable> getExecutables(Class<?> clazz) {
public List<CallableDeclaration<?>> getExecutables(String className, String sourcePath)
throws FileNotFoundException {
final List<CallableDeclaration<?>> 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<ClassOrInterfaceDeclaration> definitionOpt = cu.getClassByName(className);
Optional<? extends NodeWithConstructors<?>> 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()) {
Expand All @@ -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);
}

/**
Expand All @@ -462,6 +465,7 @@ private Map<Executable, CallableDeclaration<?>> 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.
Expand Down Expand Up @@ -494,6 +498,29 @@ && sameParamTypes(e.getParameters(), sourceCallable.getParameters()))
return map;
}

private void filterOutEnumMethods(
List<Executable> reflectionExecutables, List<CallableDeclaration<?>> sourceExecutables) {
final List<String> 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}.
*
Expand Down Expand Up @@ -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.
Expand All @@ -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<ImportDeclaration> imports = cu.getImports();
Expand All @@ -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?");
Expand Down
111 changes: 111 additions & 0 deletions src/test/java/org/toradocu/extractor/JavadocExtractorOnEnumTest.java
Original file line number Diff line number Diff line change
@@ -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<DocumentedExecutable> 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<DocumentedParameter> 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<ParamTag> 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<ThrowsTag> 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<String> 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);
}
}
13 changes: 13 additions & 0 deletions src/test/resources/example/AnEnum.java
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 23b4bd9

Please sign in to comment.