blob: 221faea4a4027e4979bcd8b707ce71f9cd5553d9 [file] [log] [blame]
package org.chromium.devtools.jsdoc.checks;
import com.google.common.base.Joiner;
import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class MethodAnnotationChecker extends ContextTrackingChecker {
private static final Pattern PARAM_PATTERN =
Pattern.compile("^[^@\n]*@param\\s+(\\{.+\\}\\s+)?([^\\s]+).*$", Pattern.MULTILINE);
private static final Pattern INVALID_RETURN_PATTERN =
Pattern.compile("^[^@\n]*(@)return(?:s.*|\\s+[^{]*)$", Pattern.MULTILINE);
private final Set<FunctionRecord> valueReturningFunctions = new HashSet<>();
private final Set<FunctionRecord> throwingFunctions = new HashSet<>();
@Override
public void enterNode(Node node) {
switch (node.getToken()) {
case FUNCTION:
handleFunction(node);
break;
case RETURN:
handleReturn(node);
break;
case THROW:
handleThrow();
break;
default:
break;
}
}
private void handleFunction(Node functionNode) {
FunctionRecord function = getState().getCurrentFunctionRecord();
if (function == null || function.parameterNames.size() == 0) {
return;
}
String[] nonAnnotatedParams = getNonAnnotatedParamData(function);
if (nonAnnotatedParams.length > 0
&& function.parameterNames.size() != nonAnnotatedParams.length) {
reportErrorAtOffset(function.info.getOriginalCommentPosition(),
String.format("No @param JSDoc tag found for parameters: [%s]",
Joiner.on(',').join(nonAnnotatedParams)));
}
}
private String[] getNonAnnotatedParamData(FunctionRecord function) {
if (function.info == null) {
return new String[0];
}
Set<String> formalParamNames = new HashSet<>();
for (int i = 0; i < function.parameterNames.size(); ++i) {
String paramName = function.parameterNames.get(i);
/**
* Varargs can be specified as `...varargs`, but will be named as
* `varargs` in the JSDoc
*/
if (paramName.startsWith("...")) {
paramName = paramName.substring(3);
}
if (!formalParamNames.add(paramName)) {
reportErrorAtNodeStart(function.functionNode,
String.format("Duplicate function argument name: %s", paramName));
}
}
Matcher m = PARAM_PATTERN.matcher(function.info.getOriginalCommentString());
while (m.find()) {
String paramType = m.group(1);
if (paramType == null) {
reportErrorAtOffset(function.info.getOriginalCommentPosition() + m.start(2),
String.format("Invalid @param annotation found -"
+ " should be \"@param {<type>} paramName\""));
} else {
formalParamNames.remove(m.group(2));
}
}
return formalParamNames.toArray(new String[formalParamNames.size()]);
}
private void handleReturn(Node node) {
if (node.getFirstChild() == null || AstUtil.parentOfType(node, Token.ASSIGN) != null) {
return;
}
FunctionRecord record = getState().getCurrentFunctionRecord();
if (record == null) {
return;
}
Node nameNode = getFunctionNameNode(record.functionNode);
if (nameNode == null) {
return;
}
valueReturningFunctions.add(record);
}
private void handleThrow() {
FunctionRecord record = getState().getCurrentFunctionRecord();
if (record == null) {
return;
}
Node nameNode = getFunctionNameNode(record.functionNode);
if (nameNode == null) {
return;
}
throwingFunctions.add(record);
}
@Override
public void leaveNode(Node node) {
if (node.getToken() != 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;
}
String[] parts = functionName.split("\\.");
functionName = parts[parts.length - 1];
boolean isApiFunction = !functionName.startsWith("_")
&& (function.isTopLevelFunction()
|| (function.enclosingType != null
&& isPlainTopLevelFunction(
function.enclosingFunctionRecord)));
boolean isReturningFunction = valueReturningFunctions.contains(function);
boolean isInterfaceFunction =
function.enclosingType != null && function.enclosingType.isInterface();
int invalidAnnotationIndex = invalidReturnAnnotationIndex(function.info);
if (invalidAnnotationIndex != -1) {
String suggestedResolution = (isReturningFunction || isInterfaceFunction)
? "should be \"@return {<type>}\""
: "please remove, as function does not return value";
getContext().reportErrorAtOffset(
function.info.getOriginalCommentPosition() + invalidAnnotationIndex,
String.format(
"invalid return type annotation found - %s", suggestedResolution));
return;
}
Node functionNameNode = getFunctionNameNode(function.functionNode);
if (functionNameNode == null) {
return;
}
if (isReturningFunction) {
if (!function.isConstructor() && !function.hasReturnAnnotation() && isApiFunction) {
reportErrorAtNodeStart(functionNameNode,
"@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)) {
reportErrorAtNodeStart(functionNameNode,
"@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(Node functionNode) {
Node nameNode = getFunctionNameNode(functionNode);
return nameNode == null ? null : getState().getNodeText(nameNode);
}
private static int invalidReturnAnnotationIndex(JSDocInfo info) {
if (info == null) {
return -1;
}
Matcher m = INVALID_RETURN_PATTERN.matcher(info.getOriginalCommentString());
return m.find() ? m.start(1) : -1;
}
private static Node getFunctionNameNode(Node functionNode) {
// FIXME: Do not require annotation for assignment-RHS functions.
return AstUtil.getFunctionNameNode(functionNode);
}
}