After understanding your question and researching this subject, I finally found what I thought to be one of the best tools to do this job. With this not only you can find each throws instance that you haven't documented, but you can also find where you don't throw anything but accidentally document a throw value.
The idea behind this is to parse the code into an abstract syntax tree. Then look for methods and look for throws statement in the methods. If a method have any throw statement, extract the exception name from those statements. Then get the Javadoc for that method. Check the Javadoc for all the @throw tags and get the name of the exception that been documented. After that, compare the exception throws versus the one that been documented. The last, you kind of have to figure that out on your own depend on your usage circumstance.
The tool I used for this is JavaParser. You can find them on Github at https://github.com/javaparser/javaparser. I downloaded their latest version. Their website is at https://javaparser.org/. They wrote a book on this subject and they mentioned that you can pay $0 dollar for the book. However, I didn't read that as they also have a Javadoc version for their program which can be found at https://www.javadoc.io/doc/com.github.javaparser/javaparser-core/3.15.1.
I wrote a demonstrate code below. In no mean that this code is final. It is just an example. You have to fix it into making it work for your case. I didn't take into consideration of nested classes, nested method, or methods within classes that are within a method. Also, the example code was written for class only and not interface. However, it is easy to adapt the code to change to able to handle interfaces.
For this, you would need to download javaParser, build it, and have their javaparser-core-3.15.1.jar or whichever version in your classpath.
The demonstrated code is below and the test.java is a file from a project that I wrote but you could use any. I also included comments in the example code.
import com.github.javaparser.*;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.comments.*;
import com.github.javaparser.ast.stmt.*;
import com.github.javaparser.ast.body.*;
import com.github.javaparser.javadoc.*;
import java.io.IOException;
import java.nio.file.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.stream.Collectors;
class Main{
public static void main(String[] args) throws IOException {
// Set file path
Path path = Paths.get("test.java");
// Set configuration
ParserConfiguration parseConfig = new ParserConfiguration();
parseConfig.setCharacterEncoding(Charset.forName("UTF-8"));
parseConfig.setTabSize(4);
parseConfig.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8);
// Get the parser
JavaParser jvParser = new JavaParser(parseConfig);
// Parse the result
ParseResult<CompilationUnit> parseResult = jvParser.parse(path);
// Check for problem
if ( !parseResult.isSuccessful() ) {
System.out.print("Parsing java code fail with the following problems:");
List<Problem> problems = parseResult.getProblems();
for ( Problem problem : problems ){
System.out.println(problem.getMessage());
}
return;
}
// Get the compilationUnit
// No optional checking for Optional<CompilationUnit> due to already check above.
CompilationUnit compilationUnit = parseResult.getResult().get();
// Get Classes
List<ClassOrInterfaceDeclaration> classes = compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
.filter(c -> !c.isInterface())
.collect(Collectors.toList());
// Traverse through each class to get method
for ( ClassOrInterfaceDeclaration c : classes ) {
// Get methods
List<MethodDeclaration> methods = c.getMethods();
for ( MethodDeclaration method : methods ) {
// Get the body statement
Optional <BlockStmt> body = method.getBody();
// if no body continue
if ( !body.isPresent() ) continue;
// After getting the body of the method code
// Search for the throw statements.
List<ThrowStmt> throwStatements = body.get().findAll(ThrowStmt.class);
// No throw statements, skip
if ( throwStatements.size() == 0 ) continue;
// Storing name of exceptions thrown into this list.
List<String> exceptionsThrown = new ArrayList<String>();
for ( ThrowStmt stmt : throwStatements ){
// Convert the throw expression to object creation expression and get the type.
String exceptionName = stmt.getExpression().asObjectCreationExpr().getType().toString();
if ( !exceptionsThrown.contains(exceptionName) ) exceptionsThrown.add(exceptionName);
}
/*
* Debug block for up to this point
System.out.println(method.getName());
System.out.println(exceptionsThrown);
System.out.println();
*
**/
// Get The Javadoc
Optional<Javadoc> javadoc = method.getJavadoc();
// To store the throws Tags
List<JavadocBlockTag> throwTags;
// A list of thrown exception that been documented.
List<String> exceptionsDocumented = new ArrayList<String>();
if ( javadoc.isPresent() ) {
throwTags = javadoc.get()
.getBlockTags()
.stream()
.filter(t -> t.getType() == JavadocBlockTag.Type.THROWS)
.collect(Collectors.toList());
for ( JavadocBlockTag tag : throwTags ) {
/*
* This may be buggy as
* the code assumed @throw exception
* to be on its own line. Therefore
* it will just take the first line as the exception name.
*/
String exceptionName = tag.getContent().toText()
.split("\n")[0]; // Use system line separator or change
// line accordingly.
if ( !exceptionsDocumented.contains(exceptionName) )
exceptionsDocumented.add(exceptionName);
}
}
// getBegin can extract the line out. But evaluating the optional would take some more code
// and is just for example so this was done like this without any checking.
System.out.println("Method: " + method.getName() + " at line " + method.getBegin());
System.out.println("Throws Exceptions: ");
System.out.println(exceptionsThrown);
System.out.println("Documented Exceptions:");
System.out.println(exceptionsDocumented);
System.out.println(System.lineSeparator() + System.lineSeparator());
}
}
}
}
test.java content:
package host.fai.lib.faiNumber;
/*
* Copyright 2019 Khang Hoang Nguyen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
**/
/**
* <p>The <code>Base2Util</code> class is a final class that provides
* static methods for converting base 2 numbering system values in
* string representation to a Java's Primitive Data Type.
*
* <p>Currently this class supports converting base 2 numbers values
* in string representation to integer int values and integer
* long values.
*
* <p>This class can parse unsigned base 2 numbers to a supported
* integer signed type as if the integer type is unsigned. However,
* some of the values must be interprete properly to get the correct
* result.
*
* <p>Example for interpreting signed value as unsigned value.
*
* <p>It is possible to store the value of 18446744073709551615L
* into a long(signed) value. However, if that value is stored into a
* signed long integer type and if we were to interprete the value
* normally, we would get a -1L value. However, if the -1L value is
* pass to LongUtil.toStringAsUnsigned, we would get
* 18446744073709551615 in string format.
*
* <p>The following example is to get to -1L. First, we assign a value
* of 9223372036854775807L to an interger long variable, multiply that
* variable to 2L, and add 1L to it.
* <pre>
* long a = 9223372036854775807L * 2L + 1L;
* System.out.println(a);
* System.out.println(LongUtil.toStringAsUnsigned(a));
* </pre>
*
* <p>Example methods for interprete signed type as unsigned type
* in a decimal strings value are
* {@link IntUtil#toStringAsUnsigned(int) IntUtil.toStringAsUnsigned}
* and {@link LongUtil#toStringAsUnsigned(long) LongUtil.toStringAsUnsigned}.
* </p>
*
* @author Khang Hoang Nguyen
*
* @since 1.0.0.f
**/
public final class Base2Util{
private Base2Util(){};
/**
* Parse the input string as signed base 2 digits representation
* into an integer int value.
*
* @param input
* A string to be parsed as signed base 2 number to an
* integer int value.
*
* @return An integer int value of the signed base 2 number
* {@code input} string.
*
* @throws NumberFormatException
* If the {@code input} string contains invalid signed
* base 2 digits, if the {@code input} string contains a
* value that is smaller than the value of Integer.MIN_VALUE(
* {@value java.lang.Integer#MIN_VALUE}),
* or if the {@code input} string contains a value that
* is larger than the value of Integer.MAX_VALUE(
* {@value java.lang.Integer#MAX_VALUE}).
*
* @throws EmptyStringException
* If the {@code input} string is empty.
*
* @since 1.0.0.f
**/
public static final int toInt(final String input){
final int length = input.length();
if ( length == 0 ) throw new EmptyStringException();
final char ch1 = input.charAt(0); int start;
if ( ch1 == '-' || ch1 == '+' ){
if ( length == 1 ) throw new NumberFormatException(input);
start = 1;
} else {
start = 0;
}
int out = 0, c;
while ( start < length && input.charAt(start) == '0' ) start++;
final int runlen = length - start;
if ( runlen > 31 ){
if ( runlen > 32 ) throw new NumberFormatException(input);
if ( ch1 != '-' ) throw new NumberFormatException(input);
if ( input.charAt(start++) != '1') throw new NumberFormatException(input);
for ( ; start < length; start++){
if ( input.charAt(start) != '0' ) throw new NumberFormatException(input);
}
return -2147483648;
}
for ( ; start < length; start++){
c = (input.charAt(start) ^ '0');
if ( c > 1 ) throw new NumberFormatException(input);
out = (out << 1) | c;
}
if ( ch1 == '-' ) return ~out + 1;
return out;
}
/**
* Parse the input string as unsigned base 2 number representation
* into an integer int value as if the integer int is an unsigned
* type. For values that need to be interpreted correctly, see the
* {@link IntUtil#toStringAsUnsigned(int) toStringAsUnsigned} method
* of the {@link IntUtil IntUtil} class.
*
* @param input
* A string to be parsed as unsigned base 2 number to an
* integer int value as if the integer int is an unsigned
* type.
*
* @return An int value that represents an unsigned integer int
* value of the unsigned base 2 number {@code input} string.
*
* @throws NumberFormatException
* If the {@code input} string contains invalid unsigned
* base 2 digits, if the {@code input} string contains a
* value that is beyond the capacity of the integer int
* data type.
*
* @throws EmptyStringException
* If the {@code input} string is empty.
*
* @since 1.0.0.f
**/
public static final int toIntAsUnsigned(final String input){
final int length = input.length();
if ( length == 0 ) throw new EmptyStringException();
int start = 0;
int out = 0, c;
while ( start < length && input.charAt(start) == '0' ) start++;
if ( length - start > 32 ) throw new NumberFormatException(input);
for ( ; start < length; start++){
c = (input.charAt(start) ^ '0');
if ( c > 1 ) throw new NumberFormatException(input);
out = (out << 1) | c;
}
return out;
}
/**
* Parse the input string as signed base 2 number representation
* into an integer long value.
*
* @param input
* A string to be parsed as signed base 2 number to an
* integer long value.
*
* @return An integer long value of the signed base 2 number
* {@code input} string.
*
* @throws NumberFormatException
* If the {@code input} string contains invalid signed
* base 2 digits, if the {@code input} string contains a
* value that is smaller than the value of Long.MIN_VALUE(
* {@value java.lang.Long#MIN_VALUE}), or if
* the {@code input} string contains a value that is larger
* than the value of Long.MAX_VALUE(
* {@value java.lang.Long#MAX_VALUE}).
*
* @throws EmptyStringException
* If the {@code input} string is empty.
*
* @since 1.0.0.f
**/
public static final long toLong(final String input){
final int length = input.length();
if ( length == 0 ) throw new EmptyStringException();
final char ch1 = input.charAt(0); int start = 0;
if ( ch1 == '-' || ch1 == '+' ){
if ( length == 1 ) throw new NumberFormatException(input);
start = 1;
}
long out = 0, c;
while ( start < length && input.charAt(start) == '0' ) start++;
final int runlen = length - start;
if ( runlen > 63 ){
if ( runlen > 64 ) throw new NumberFormatException(input);
if ( ch1 != '-' ) throw new NumberFormatException(input);
if ( input.charAt(start++) != '1') throw new NumberFormatException(input);
for ( ; start < length; start++){
if ( input.charAt(start) != '0' ) throw new NumberFormatException(input);
}
return -9223372036854775808L;
}
for ( ; start < length; start++){
c = (input.charAt(start) ^ '0');
if ( c > 1L ) throw new NumberFormatException(input);
out = (out << 1) | c;
}
if ( ch1 == '-' ) return ~out + 1L;
return out;
}
/**
* Parse the input string as unsigned base 2 number representation
* into an integer long value as if the integer long is an unsigned
* type. For values that need to be interpreted correctly, see the
* {@link LongUtil#toStringAsUnsigned(long) toStringAsUnsigned} method
* of the {@link LongUtil LongUtil} class.
*
* @param input
* A string to be parsed as unsigned base 2 number to an
* integer long value as if the integer long is an unsigned
* type.
*
* @return An integer long value represent the unsigned integer
* long value of the unsigned base 2 number {@code input}
* string.
*
* @throws NumberFormatException
* If the {@code input} string contains invalid unsigned
* base 2 digits, or if the {code input} string
* contains a value that is beyond the capacity of the
* long data type.
*
* @throws EmptyStringException
* If the {@code input} string is empty.
*
* @since 1.0.0.f
**/
public static final long toLongAsUnsigned(final String input){
final int length = input.length();
if ( length == 0 ) throw new EmptyStringException();
int start = 0;
long out = 0, c;
while ( start < length && input.charAt(start) == '0' ) start++;
if ( length - start > 64 ) throw new NumberFormatException(input);
for ( ; start < length; start++){
c = (input.charAt(start) ^ '0');
if ( c > 1L ) throw new NumberFormatException(input);
out = (out << 1) | c;
}
return out;
}
}
throws
clauses? If it's because you're expecting people to handle them then they shouldn't be Runtime exceptions at all. If you're not, then why are you declaring them if you're not expecting the caller to handle them? – Maraudingthrows
clause. 1. do you really also want to check the Javadocs for missing@throws ...
documentation 2. How far do you go? Throwingthrows NullPointerException
at a method seems odd, at least (even though it can be very reasonable in the JavaDocs!), and when you are calling a third-party method, you could have a hard time figuring out what could be thrown, because this is usually not declared viathrows
, and too often not even with the@throws
JavaDocs... – Critique