Index: Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ReturnAnnotationChecker.java |
diff --git a/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ReturnAnnotationChecker.java b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ReturnAnnotationChecker.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3b8c300dd79626a03ec90b75721b2ccfa1aa34b8 |
--- /dev/null |
+++ b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ReturnAnnotationChecker.java |
@@ -0,0 +1,145 @@ |
+package org.chromium.devtools.jsdoc.checks; |
+ |
+import com.google.javascript.rhino.head.Token; |
+import com.google.javascript.rhino.head.ast.AstNode; |
+import com.google.javascript.rhino.head.ast.Comment; |
+import com.google.javascript.rhino.head.ast.FunctionNode; |
+import com.google.javascript.rhino.head.ast.ObjectProperty; |
+import com.google.javascript.rhino.head.ast.ReturnStatement; |
+ |
+import java.util.HashSet; |
+import java.util.Set; |
+ |
+public final class ReturnAnnotationChecker extends ContextTrackingChecker { |
+ |
+ private final Set<FunctionRecord> valueReturningFunctions = new HashSet<>(); |
+ private final Set<FunctionRecord> throwingFunctions = new HashSet<>(); |
+ |
+ @Override |
+ public void enterNode(AstNode node) { |
+ switch (node.getType()) { |
+ case Token.RETURN: |
+ handleReturn((ReturnStatement) node); |
+ break; |
+ case Token.THROW: |
+ handleThrow(); |
+ break; |
+ default: |
+ break; |
+ } |
+ } |
+ |
+ private void handleReturn(ReturnStatement node) { |
+ if (node.getReturnValue() == null || AstUtil.hasParentOfType(node, Token.ASSIGN)) { |
+ return; |
+ } |
+ |
+ FunctionRecord record = getState().getCurrentFunctionRecord(); |
+ if (record == null) { |
+ return; |
+ } |
+ AstNode nameNode = getFunctionNameNode(record.functionNode); |
+ if (nameNode == null) { |
+ return; |
+ } |
+ valueReturningFunctions.add(record); |
+ } |
+ |
+ private void handleThrow() { |
+ FunctionRecord record = getState().getCurrentFunctionRecord(); |
+ if (record == null) { |
+ return; |
+ } |
+ AstNode nameNode = getFunctionNameNode(record.functionNode); |
+ if (nameNode == null) { |
+ return; |
+ } |
+ throwingFunctions.add(record); |
+ } |
+ |
+ @Override |
+ public void leaveNode(AstNode node) { |
+ if (node.getType() != Token.FUNCTION) { |
+ return; |
+ } |
+ |
+ FunctionRecord record = getState().getCurrentFunctionRecord(); |
+ if (record != null) { |
+ checkFunctionAnnotation(record); |
+ } |
+ } |
+ |
+ @SuppressWarnings("unused") |
+ private void checkFunctionAnnotation(FunctionRecord function) { |
+ String functionName = getFunctionName(function.functionNode); |
+ if (functionName == null) { |
+ return; |
+ } |
+ boolean isApiFunction = !functionName.startsWith("_") |
+ && (function.isTopLevelFunction() |
+ || (function.enclosingType != null |
+ && isPlainTopLevelFunction(function.enclosingFunctionRecord))); |
+ Comment jsDocNode = AstUtil.getJsDocNode(function.functionNode); |
+ |
+ boolean isReturningFunction = valueReturningFunctions.contains(function); |
+ boolean isInterfaceFunction = |
+ function.enclosingType != null && function.enclosingType.isInterface; |
+ int invalidAnnotationIndex = |
+ invalidReturnsAnnotationIndex(getState().getNodeText(jsDocNode)); |
+ if (invalidAnnotationIndex != -1) { |
+ String suggestedResolution = (isReturningFunction || isInterfaceFunction) |
+ ? "should be @return instead" |
+ : "please remove, as function does not return value"; |
+ getContext().reportErrorInNode(jsDocNode, invalidAnnotationIndex, |
+ String.format("invalid @returns annotation found - %s", suggestedResolution)); |
+ return; |
+ } |
+ AstNode functionNameNode = getFunctionNameNode(function.functionNode); |
+ if (functionNameNode == null) { |
+ return; |
+ } |
+ |
+ if (isReturningFunction) { |
+ if (!function.hasReturnAnnotation() && isApiFunction) { |
+ getContext().reportErrorInNode(functionNameNode, 0, |
+ "@return annotation is required for API functions that return value"); |
+ } |
+ } else { |
+ // A @return-function that does not actually return anything and |
+ // is intended to be overridden in subclasses must throw. |
+ if (function.hasReturnAnnotation() |
+ && !isInterfaceFunction |
+ && !throwingFunctions.contains(function)) { |
+ getContext().reportErrorInNode(functionNameNode, 0, |
+ "@return annotation found, yet function does not return value"); |
+ } |
+ } |
+ } |
+ |
+ private static boolean isPlainTopLevelFunction(FunctionRecord record) { |
+ return record != null && record.isTopLevelFunction() |
+ && (record.enclosingType == null && !record.isConstructor); |
+ } |
+ |
+ private String getFunctionName(FunctionNode functionNode) { |
+ AstNode nameNode = getFunctionNameNode(functionNode); |
+ return nameNode == null ? null : getState().getNodeText(nameNode); |
+ } |
+ |
+ private static int invalidReturnsAnnotationIndex(String jsDoc) { |
+ return jsDoc == null ? -1 : jsDoc.indexOf("@returns"); |
+ } |
+ |
+ private static AstNode getFunctionNameNode(FunctionNode functionNode) { |
+ AstNode nameNode = functionNode.getFunctionName(); |
+ if (nameNode != null) { |
+ return nameNode; |
+ } |
+ |
+ if (AstUtil.hasParentOfType(functionNode, Token.COLON)) { |
+ return ((ObjectProperty) functionNode.getParent()).getLeft(); |
+ } |
+ // Do not require annotation for assignment-RHS functions. |
+ return null; |
+ } |
+} |