1f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)package org.chromium.devtools.jsdoc.checks; 2f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 3f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import com.google.common.base.Joiner; 4197021e6b966cfb06891637935ef33fff06433d1Ben Murdochimport com.google.javascript.jscomp.NodeUtil; 5197021e6b966cfb06891637935ef33fff06433d1Ben Murdochimport com.google.javascript.rhino.JSDocInfo; 6197021e6b966cfb06891637935ef33fff06433d1Ben Murdochimport com.google.javascript.rhino.Node; 7197021e6b966cfb06891637935ef33fff06433d1Ben Murdochimport com.google.javascript.rhino.Token; 8f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 9f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import java.util.HashSet; 10f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import java.util.Set; 11f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import java.util.regex.Matcher; 12f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import java.util.regex.Pattern; 13f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 14f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)public final class MethodAnnotationChecker extends ContextTrackingChecker { 15f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 16f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) private static final Pattern PARAM_PATTERN = 17f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) Pattern.compile("^[^@\n]*@param\\s+(\\{.+\\}\\s+)?([^\\s]+).*$", Pattern.MULTILINE); 18f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 19f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) private static final Pattern INVALID_RETURN_PATTERN = 20f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) Pattern.compile("^[^@\n]*(@)return(?:s.*|\\s+[^{]*)$", Pattern.MULTILINE); 21f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 22f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) private final Set<FunctionRecord> valueReturningFunctions = new HashSet<>(); 23f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) private final Set<FunctionRecord> throwingFunctions = new HashSet<>(); 24f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 25f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) @Override 26197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch public void enterNode(Node node) { 27f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) switch (node.getType()) { 28f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) case Token.FUNCTION: 29197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch handleFunction(node); 30f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) break; 31f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) case Token.RETURN: 32197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch handleReturn(node); 33f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) break; 34f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) case Token.THROW: 35f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) handleThrow(); 36f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) break; 37f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) default: 38f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) break; 39f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 40f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 41f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 42197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch private void handleFunction(Node functionNode) { 43197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Node parametersNode = NodeUtil.getFunctionParameters(functionNode); 44197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch int actualParamCount = parametersNode.getChildCount(); 45f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (actualParamCount == 0) { 46f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 47f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 48197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch JSDocInfo jsDocInfo = NodeUtil.getBestJSDocInfo(functionNode); 49197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch String[] nonAnnotatedParams = getNonAnnotatedParamData(parametersNode, jsDocInfo); 50197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch if (nonAnnotatedParams.length > 0 && actualParamCount != nonAnnotatedParams.length) { 51197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch reportErrorAtOffset(jsDocInfo.getOriginalCommentPosition(), 52197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch String.format( 53197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch "No @param JSDoc tag found for parameters: [%s]", 54197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Joiner.on(',').join(nonAnnotatedParams))); 55f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 56f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 57f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 58197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch private String[] getNonAnnotatedParamData(Node params, JSDocInfo info) { 59197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch if (info == null) { 60f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return new String[0]; 61f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 62197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Set<String> formalParamNames = new HashSet<>(); 63197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch for (int i = 0, childCount = params.getChildCount(); i < childCount; ++i) { 64197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Node paramNode = params.getChildAtIndex(i); 65f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) String paramName = getContext().getNodeText(paramNode); 66197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch if (!formalParamNames.add(paramName)) { 67f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) reportErrorAtNodeStart(paramNode, 68f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) String.format("Duplicate function argument name: %s", paramName)); 69f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 70f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 71197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Matcher m = PARAM_PATTERN.matcher(info.getOriginalCommentString()); 72f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) while (m.find()) { 73f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) String paramType = m.group(1); 74f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (paramType == null) { 75197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch reportErrorAtOffset(info.getOriginalCommentPosition() + m.start(2), 76197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch String.format( 77197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch "Invalid @param annotation found -" 78197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch + " should be \"@param {<type>} paramName\"")); 79f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } else { 80197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch formalParamNames.remove(m.group(2)); 81f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 82f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 83197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch return formalParamNames.toArray(new String[formalParamNames.size()]); 84f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 85f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 86197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch private void handleReturn(Node node) { 87197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch if (node.getFirstChild() == null || AstUtil.parentOfType(node, Token.ASSIGN) != null) { 88f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 89f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 90f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 91f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) FunctionRecord record = getState().getCurrentFunctionRecord(); 92f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (record == null) { 93f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 94f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 95197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Node nameNode = getFunctionNameNode(record.functionNode); 96f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (nameNode == null) { 97f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 98f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 99f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) valueReturningFunctions.add(record); 100f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 101f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 102f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) private void handleThrow() { 103f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) FunctionRecord record = getState().getCurrentFunctionRecord(); 104f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (record == null) { 105f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 106f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 107197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Node nameNode = getFunctionNameNode(record.functionNode); 108f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (nameNode == null) { 109f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 110f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 111f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) throwingFunctions.add(record); 112f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 113f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 114f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) @Override 115197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch public void leaveNode(Node node) { 116f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (node.getType() != Token.FUNCTION) { 117f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 118f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 119f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 120f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) FunctionRecord record = getState().getCurrentFunctionRecord(); 121f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (record != null) { 122f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) checkFunctionAnnotation(record); 123f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 124f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 125f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 126f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) @SuppressWarnings("unused") 127f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) private void checkFunctionAnnotation(FunctionRecord function) { 128f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) String functionName = getFunctionName(function.functionNode); 129f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (functionName == null) { 130f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 131f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 132197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch String[] parts = functionName.split("\\."); 133197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch functionName = parts[parts.length - 1]; 134f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) boolean isApiFunction = !functionName.startsWith("_") 135f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) && (function.isTopLevelFunction() 136f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) || (function.enclosingType != null 137f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) && isPlainTopLevelFunction(function.enclosingFunctionRecord))); 138f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 139f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) boolean isReturningFunction = valueReturningFunctions.contains(function); 140f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) boolean isInterfaceFunction = 141197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch function.enclosingType != null && function.enclosingType.isInterface(); 142f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) int invalidAnnotationIndex = 143197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch invalidReturnAnnotationIndex(function.info); 144f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (invalidAnnotationIndex != -1) { 145f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) String suggestedResolution = (isReturningFunction || isInterfaceFunction) 146f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) ? "should be \"@return {<type>}\"" 147f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) : "please remove, as function does not return value"; 148197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch getContext().reportErrorAtOffset( 149197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch function.info.getOriginalCommentPosition() + invalidAnnotationIndex, 150f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) String.format( 151f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) "invalid return type annotation found - %s", suggestedResolution)); 152f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 153f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 154197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Node functionNameNode = getFunctionNameNode(function.functionNode); 155f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (functionNameNode == null) { 156f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return; 157f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 158f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 159f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (isReturningFunction) { 160197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch if (!function.isConstructor() && !function.hasReturnAnnotation() && isApiFunction) { 161f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) reportErrorAtNodeStart( 162f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) functionNameNode, 163f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) "@return annotation is required for API functions that return value"); 164f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 165f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } else { 166f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) // A @return-function that does not actually return anything and 167f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) // is intended to be overridden in subclasses must throw. 168f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) if (function.hasReturnAnnotation() 169f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) && !isInterfaceFunction 170f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) && !throwingFunctions.contains(function)) { 171f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) reportErrorAtNodeStart(functionNameNode, 172f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) "@return annotation found, yet function does not return value"); 173f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 174f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 175f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 176f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 177f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) private static boolean isPlainTopLevelFunction(FunctionRecord record) { 178f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return record != null && record.isTopLevelFunction() 179197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch && (record.enclosingType == null && !record.isConstructor()); 180f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 181f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 182197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch private String getFunctionName(Node functionNode) { 183197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Node nameNode = getFunctionNameNode(functionNode); 184f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return nameNode == null ? null : getState().getNodeText(nameNode); 185f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 186f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 187197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch private static int invalidReturnAnnotationIndex(JSDocInfo info) { 188197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch if (info == null) { 189f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return -1; 190f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 191197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch Matcher m = INVALID_RETURN_PATTERN.matcher(info.getOriginalCommentString()); 192f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) return m.find() ? m.start(1) : -1; 193f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 194f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) 195197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch private static Node getFunctionNameNode(Node functionNode) { 196197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch // FIXME: Do not require annotation for assignment-RHS functions. 197197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch return AstUtil.getFunctionNameNode(functionNode); 198f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles) } 199f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)} 200