| 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;
|
| + }
|
| +}
|
|
|