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