PageRenderTime 94ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/JavadocThrowsChecks.java

http://github.com/WindowsAzure/azure-sdk-for-java
Java | 334 lines | 238 code | 47 blank | 49 comment | 76 complexity | cedf9e390ef2dd549a494b666b2956b9 MD5 | raw file
Possible License(s): MIT
  1. // Copyright (c) Microsoft Corporation. All rights reserved.
  2. // Licensed under the MIT License.
  3. package com.azure.tools.checkstyle.checks;
  4. import com.puppycrawl.tools.checkstyle.DetailNodeTreeStringPrinter;
  5. import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
  6. import com.puppycrawl.tools.checkstyle.api.DetailAST;
  7. import com.puppycrawl.tools.checkstyle.api.DetailNode;
  8. import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
  9. import com.puppycrawl.tools.checkstyle.api.TokenTypes;
  10. import com.puppycrawl.tools.checkstyle.utils.BlockCommentPosition;
  11. import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
  12. import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
  13. import java.util.HashMap;
  14. import java.util.HashSet;
  15. import java.util.Map;
  16. public class JavadocThrowsChecks extends AbstractCheck {
  17. private static final String MISSING_DESCRIPTION_MESSAGE =
  18. "@throws tag requires a description explaining when the error is thrown.";
  19. private static final String MISSING_THROWS_TAG_MESSAGE = "Javadoc @throws tag required for unchecked throw.";
  20. private static final int[] TOKENS = new int[] {
  21. TokenTypes.CTOR_DEF,
  22. TokenTypes.METHOD_DEF,
  23. TokenTypes.BLOCK_COMMENT_BEGIN,
  24. TokenTypes.LITERAL_THROWS,
  25. TokenTypes.LITERAL_THROW,
  26. TokenTypes.PARAMETER_DEF,
  27. TokenTypes.VARIABLE_DEF,
  28. };
  29. private static final String THIS_TOKEN = "this";
  30. private static final String CLASS_TOKEN = "class";
  31. private Map<String, HashSet<String>> javadocThrowsMapping;
  32. private Map<String, HashSet<String>> exceptionMapping;
  33. private String currentScopeIdentifier;
  34. private boolean currentScopeNeedsChecking;
  35. @Override
  36. public int[] getDefaultTokens() {
  37. return getRequiredTokens();
  38. }
  39. @Override
  40. public int[] getAcceptableTokens() {
  41. return getRequiredTokens();
  42. }
  43. @Override
  44. public int[] getRequiredTokens() {
  45. return TOKENS;
  46. }
  47. @Override
  48. public boolean isCommentNodesRequired() {
  49. return true;
  50. }
  51. @Override
  52. public void beginTree(DetailAST rootToken) {
  53. javadocThrowsMapping = new HashMap<>();
  54. exceptionMapping = new HashMap<>();
  55. currentScopeNeedsChecking = false;
  56. currentScopeIdentifier = "";
  57. }
  58. @Override
  59. public void visitToken(DetailAST token) {
  60. switch (token.getType()) {
  61. case TokenTypes.CTOR_DEF:
  62. case TokenTypes.METHOD_DEF:
  63. setIdentifierAndCheckStatus(token);
  64. break;
  65. case TokenTypes.BLOCK_COMMENT_BEGIN:
  66. findJavadocThrows(token);
  67. break;
  68. case TokenTypes.LITERAL_THROWS:
  69. if (currentScopeNeedsChecking) {
  70. verifyCheckedThrowJavadoc(token);
  71. }
  72. break;
  73. case TokenTypes.LITERAL_THROW:
  74. if (currentScopeNeedsChecking) {
  75. verifyUncheckedThrowJavadoc(token);
  76. }
  77. break;
  78. case TokenTypes.PARAMETER_DEF:
  79. case TokenTypes.VARIABLE_DEF:
  80. if (currentScopeNeedsChecking || token.getParent().getType() == TokenTypes.OBJBLOCK) {
  81. addExceptionMapping(token);
  82. }
  83. break;
  84. default:
  85. // Checkstyle complains if there's no default block in switch
  86. break;
  87. }
  88. }
  89. /*
  90. * Gets the current method identifier and determines if it needs to be checked.
  91. * @param scopeDefToken Method definition token.
  92. */
  93. private void setIdentifierAndCheckStatus(DetailAST scopeDefToken) {
  94. currentScopeIdentifier = scopeDefToken.findFirstToken(TokenTypes.IDENT).getText() + scopeDefToken.getLineNo();
  95. currentScopeNeedsChecking =
  96. visibilityIsPublicOrProtectedAndNotAbstractOrOverride(scopeDefToken.findFirstToken(TokenTypes.MODIFIERS));
  97. }
  98. /*
  99. * Determines if the modifiers contains either public or protected and isn't abstract or an override.
  100. * @param modifiersToken Modifiers token.
  101. * @return True if the method if public or protected and isn't abstract.
  102. */
  103. private boolean visibilityIsPublicOrProtectedAndNotAbstractOrOverride(DetailAST modifiersToken) {
  104. if (modifiersToken == null) {
  105. return false;
  106. }
  107. // Don't need to check abstract methods as they won't have implementation.
  108. if (modifiersToken.findFirstToken(TokenTypes.ABSTRACT) != null) {
  109. return false;
  110. }
  111. // Don't need to check override methods that don't have JavaDocs.
  112. if (modifiersToken.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN) == null) {
  113. if (TokenUtil.findFirstTokenByPredicate(modifiersToken, this::isOverrideAnnotation).isPresent()) {
  114. return false;
  115. }
  116. }
  117. // Check public or protect methods.
  118. return modifiersToken.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
  119. || modifiersToken.findFirstToken(TokenTypes.LITERAL_PROTECTED) != null;
  120. }
  121. private boolean isOverrideAnnotation(DetailAST modifierToken) {
  122. if (modifierToken.getType() != TokenTypes.ANNOTATION) {
  123. return false;
  124. }
  125. // Possible for an identifier not to exist if it is a nested class (ie. @Parameterized.Parameters(String)).
  126. final DetailAST identifier = modifierToken.findFirstToken(TokenTypes.IDENT);
  127. return identifier != null && "Override".equals(identifier.getText());
  128. }
  129. /*
  130. * Checks if the comment is on a method, if so it searches for the documented Javadoc @throws statements.
  131. * @param blockCommentToken Block comment token.
  132. */
  133. private void findJavadocThrows(DetailAST blockCommentToken) {
  134. if (!BlockCommentPosition.isOnMethod(blockCommentToken)
  135. && !BlockCommentPosition.isOnConstructor(blockCommentToken)) {
  136. return;
  137. }
  138. // Turn the DetailAST into a Javadoc DetailNode.
  139. DetailNode javadocNode = null;
  140. try {
  141. javadocNode = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(blockCommentToken);
  142. } catch (IllegalArgumentException ex) {
  143. // Exceptions are thrown if the JavaDoc has invalid formatting.
  144. }
  145. if (javadocNode == null) {
  146. return;
  147. }
  148. // Append the line number to differentiate overloads.
  149. HashSet<String> javadocThrows = javadocThrowsMapping.getOrDefault(currentScopeIdentifier, new HashSet<>());
  150. // Iterate through all the top level nodes in the Javadoc, looking for the @throws statements.
  151. for (DetailNode node : javadocNode.getChildren()) {
  152. if (node.getType() != JavadocTokenTypes.JAVADOC_TAG
  153. || JavadocUtil.findFirstToken(node, JavadocTokenTypes.THROWS_LITERAL) == null) {
  154. continue;
  155. }
  156. // Add the class being thrown to the set of documented throws.
  157. javadocThrows.add(JavadocUtil.findFirstToken(node, JavadocTokenTypes.CLASS_NAME).getText());
  158. if (JavadocUtil.findFirstToken(node, JavadocTokenTypes.DESCRIPTION) == null) {
  159. log(node.getLineNumber(), MISSING_DESCRIPTION_MESSAGE);
  160. }
  161. }
  162. javadocThrowsMapping.put(currentScopeIdentifier, javadocThrows);
  163. }
  164. /*
  165. * Checks if parameter and variable definitions are exception definitions, if so adds them to the mapping.
  166. * @param definitionToken Definition token.
  167. */
  168. private void addExceptionMapping(DetailAST definitionToken) {
  169. DetailAST typeToken = definitionToken.findFirstToken(TokenTypes.TYPE).getFirstChild();
  170. // Lambdas don't list a type, quit out.
  171. if (typeToken == null) {
  172. return;
  173. }
  174. String scope = currentScopeIdentifier;
  175. if (currentScopeIdentifier == null || currentScopeIdentifier.isEmpty()) {
  176. if (definitionToken.branchContains(TokenTypes.LITERAL_STATIC)) {
  177. scope = CLASS_TOKEN;
  178. } else {
  179. scope = THIS_TOKEN;
  180. }
  181. }
  182. String identifier = scope + definitionToken.findFirstToken(TokenTypes.IDENT).getText();
  183. HashSet<String> types = exceptionMapping.getOrDefault(identifier, new HashSet<>());
  184. if (typeToken.getType() == TokenTypes.BOR) {
  185. TokenUtil.forEachChild(typeToken, TokenTypes.IDENT, (identityToken) -> tryToAddType(identityToken, types));
  186. } else {
  187. tryToAddType(typeToken, types);
  188. }
  189. exceptionMapping.put(identifier, types);
  190. }
  191. private void tryToAddType(DetailAST typeToken, HashSet<String> types) {
  192. String type = typeToken.getText();
  193. if (type.endsWith("Exception") || type.endsWith("Error")) {
  194. types.add(type);
  195. }
  196. }
  197. /*
  198. * Verifies that the checked exceptions, those in the throws statement, are documented.
  199. * @param throwsToken Throws token.
  200. */
  201. private void verifyCheckedThrowJavadoc(DetailAST throwsToken) {
  202. HashSet<String> methodJavadocThrows = javadocThrowsMapping.get(currentScopeIdentifier);
  203. if (methodJavadocThrows == null) {
  204. log(throwsToken, MISSING_THROWS_TAG_MESSAGE);
  205. return;
  206. }
  207. TokenUtil.forEachChild(throwsToken, TokenTypes.IDENT, (throwTypeToken) -> {
  208. if (!methodJavadocThrows.contains(throwTypeToken.getText())) {
  209. log(throwTypeToken, MISSING_THROWS_TAG_MESSAGE);
  210. }
  211. });
  212. }
  213. /*
  214. * Checks if the throw statement has documentation in the Javadoc.
  215. * @param throwToken Throw statement token.
  216. */
  217. private void verifyUncheckedThrowJavadoc(DetailAST throwToken) {
  218. // Early out check for method that don't have Javadocs, they cannot have @throws documented.
  219. HashSet<String> methodJavadocThrows = javadocThrowsMapping.get(currentScopeIdentifier);
  220. if (methodJavadocThrows == null) {
  221. log(throwToken, MISSING_THROWS_TAG_MESSAGE);
  222. return;
  223. }
  224. DetailAST throwExprToken = throwToken.findFirstToken(TokenTypes.EXPR);
  225. // Check if the throw is constructing the exception, method call, or throwing an instantiated exception.
  226. DetailAST literalNewToken = throwExprToken.findFirstToken(TokenTypes.LITERAL_NEW);
  227. DetailAST methodCallToken = throwExprToken.findFirstToken(TokenTypes.METHOD_CALL);
  228. DetailAST typecastToken = throwExprToken.findFirstToken(TokenTypes.TYPECAST);
  229. if (typecastToken != null) {
  230. // Throwing a casted variable.
  231. String throwType = typecastToken.findFirstToken(TokenTypes.TYPE).findFirstToken(TokenTypes.IDENT).getText();
  232. if (!methodJavadocThrows.contains(throwType)) {
  233. log(throwExprToken, MISSING_THROWS_TAG_MESSAGE);
  234. }
  235. } else if (literalNewToken != null) {
  236. // Throwing a constructed exception.
  237. if (!methodJavadocThrows.contains(literalNewToken.findFirstToken(TokenTypes.IDENT).getText())) {
  238. log(throwToken, MISSING_THROWS_TAG_MESSAGE);
  239. }
  240. } else if (methodCallToken != null) {
  241. // Throwing a method call.
  242. // Checkstyle complains about empty blocks.
  243. // TODO: Should we ignore this checkstyle error?
  244. return;
  245. } else {
  246. // Throwing an un-casted variable.
  247. DetailAST lastIdentifier = null;
  248. DetailAST current = throwExprToken;
  249. while (current != null) {
  250. if (current.getType() == TokenTypes.IDENT) {
  251. lastIdentifier = current;
  252. }
  253. if (current.getFirstChild() != null) {
  254. current = current.getFirstChild();
  255. } else {
  256. current = current.getNextSibling();
  257. }
  258. }
  259. if (lastIdentifier == null) {
  260. return;
  261. }
  262. String throwIdentName = lastIdentifier.getText();
  263. HashSet<String> types = findMatchingExceptionType(currentScopeIdentifier, throwIdentName);
  264. if (types == null) {
  265. return;
  266. }
  267. for (String type : types) {
  268. if (!methodJavadocThrows.contains(type)) {
  269. log(throwExprToken, MISSING_THROWS_TAG_MESSAGE);
  270. }
  271. }
  272. }
  273. }
  274. private HashSet<String> findMatchingExceptionType(String scope, String throwIdent) {
  275. // check current scope
  276. HashSet<String> types = exceptionMapping.get(scope + throwIdent);
  277. if (types == null) {
  278. // if a matching type is not found in current method scope, search object scope
  279. types = exceptionMapping.get(THIS_TOKEN + throwIdent);
  280. }
  281. if (types == null) {
  282. // if a matching type is not found in method or instance scope, search class scope
  283. types = exceptionMapping.get(CLASS_TOKEN + throwIdent);
  284. }
  285. return types;
  286. }
  287. }