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..2c663ae934cbb93f34299ed96fe8059ca0639a40 |
--- /dev/null |
+++ b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ReturnAnnotationChecker.java |
@@ -0,0 +1,179 @@ |
+package org.chromium.devtools.jsdoc.checks; |
+ |
+import com.google.javascript.rhino.head.Token; |
+import com.google.javascript.rhino.head.ast.Assignment; |
+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 org.chromium.devtools.jsdoc.ValidatorContext; |
+ |
+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; |
+ } |
+ } |
+ |
+ 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 record) { |
+ boolean isReturningFunction = valueReturningFunctions.contains(record); |
+ String functionName = getFunctionName(record.functionNode); |
+ if (functionName == null) { |
+ return; |
+ } |
+ boolean isApiFunction = !functionName.startsWith("_") |
+ && (record.isTopLevelFunction() |
+ || (record.enclosingType != null |
+ && isPlainTopLevelFunction(record.enclosingFunctionRecord))); |
+ boolean isInterfaceFunction = |
+ record.enclosingType != null && record.enclosingType.isInterface; |
+ Comment jsDocNode = getJsDocNode(record.functionNode); |
+ String jsDoc = jsDocNode != null ? jsDocNode.getValue() : null; |
+ |
+ int invalidAnnotationIndex = invalidReturnsAnnotationIndex(jsDoc); |
+ ValidatorContext context = getState().getContext(); |
+ if (invalidAnnotationIndex != -1) { |
+ String suggestedResolution = (isReturningFunction || isInterfaceFunction) |
+ ? "should be @return instead" |
+ : "please remove, as function does not return value"; |
+ context.reportErrorInNode(jsDocNode, invalidAnnotationIndex, |
+ String.format("invalid @returns annotation found - %s", suggestedResolution)); |
+ return; |
+ } |
+ AstNode functionNameNode = getFunctionNameNode(record.functionNode); |
+ if (functionNameNode == null) { |
+ return; |
+ } |
+ |
+ if (isReturningFunction) { |
+ if (!record.hasReturnAnnotation() && isApiFunction) { |
+ context.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 (record.hasReturnAnnotation() |
+ && !isInterfaceFunction |
+ && !throwingFunctions.contains(record)) { |
+ context.reportErrorInNode(functionNameNode, 0, |
+ "@return annotation found, yet function does not return value"); |
+ } |
+ } |
+ } |
+ |
+ private boolean isPlainTopLevelFunction(FunctionRecord record) { |
+ if (record == null) { |
+ return false; |
+ } |
+ return record.isTopLevelFunction() |
+ && (record.enclosingType == null && !record.isConstructor); |
+ } |
+ |
+ private String getFunctionName(FunctionNode functionNode) { |
+ AstNode nameNode = getFunctionNameNode(functionNode); |
+ if (nameNode == null) { |
+ return null; |
+ } |
+ return 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; |
+ } |
+ |
+ private static Comment getJsDocNode(FunctionNode functionNode) { |
+ Comment jsDocNode = functionNode.getJsDocNode(); |
+ if (jsDocNode != null) { |
+ return jsDocNode; |
+ } |
+ |
+ // reader.onloadend = function() {...} |
+ if (AstUtil.hasParentOfType(functionNode, Token.ASSIGN)) { |
+ Assignment assignment = (Assignment) functionNode.getParent(); |
+ if (assignment.getRight() == functionNode) { |
+ jsDocNode = assignment.getJsDocNode(); |
+ if (jsDocNode != null) { |
+ return jsDocNode; |
+ } |
+ } |
+ } |
+ |
+ if (AstUtil.hasParentOfType(functionNode, Token.COLON)) { |
+ jsDocNode = ((ObjectProperty) functionNode.getParent()).getLeft().getJsDocNode(); |
+ if (jsDocNode != null) { |
+ return jsDocNode; |
+ } |
+ } |
+ return null; |
+ } |
+} |