1 package org.chromium.devtools.jsdoc.checks;
3 import com.google.common.base.Joiner;
4 import com.google.javascript.jscomp.NodeUtil;
5 import com.google.javascript.rhino.JSDocInfo;
6 import com.google.javascript.rhino.Node;
7 import com.google.javascript.rhino.Token;
9 import java.util.HashSet;
11 import java.util.regex.Matcher;
12 import java.util.regex.Pattern;
14 public final class MethodAnnotationChecker extends ContextTrackingChecker {
16 private static final Pattern PARAM_PATTERN =
17 Pattern.compile("^[^@\n]*@param\\s+(\\{.+\\}\\s+)?([^\\s]+).*$", Pattern.MULTILINE);
19 private static final Pattern INVALID_RETURN_PATTERN =
20 Pattern.compile("^[^@\n]*(@)return(?:s.*|\\s+[^{]*)$", Pattern.MULTILINE);
22 private final Set<FunctionRecord> valueReturningFunctions = new HashSet<>();
23 private final Set<FunctionRecord> throwingFunctions = new HashSet<>();
26 public void enterNode(Node node) {
27 switch (node.getType()) {
42 private void handleFunction(Node functionNode) {
43 FunctionRecord function = getState().getCurrentFunctionRecord();
44 if (function == null || function.parameterNames.size() == 0) {
47 String[] nonAnnotatedParams = getNonAnnotatedParamData(function);
48 if (nonAnnotatedParams.length > 0
49 && function.parameterNames.size() != nonAnnotatedParams.length) {
50 reportErrorAtOffset(function.info.getOriginalCommentPosition(),
52 "No @param JSDoc tag found for parameters: [%s]",
53 Joiner.on(',').join(nonAnnotatedParams)));
57 private String[] getNonAnnotatedParamData(FunctionRecord function) {
58 if (function.info == null) {
61 Set<String> formalParamNames = new HashSet<>();
62 for (int i = 0; i < function.parameterNames.size(); ++i) {
63 String paramName = function.parameterNames.get(i);
64 if (!formalParamNames.add(paramName)) {
65 reportErrorAtNodeStart(function.functionNode,
66 String.format("Duplicate function argument name: %s", paramName));
69 Matcher m = PARAM_PATTERN.matcher(function.info.getOriginalCommentString());
71 String paramType = m.group(1);
72 if (paramType == null) {
73 reportErrorAtOffset(function.info.getOriginalCommentPosition() + m.start(2),
75 "Invalid @param annotation found -"
76 + " should be \"@param {<type>} paramName\""));
78 formalParamNames.remove(m.group(2));
81 return formalParamNames.toArray(new String[formalParamNames.size()]);
84 private void handleReturn(Node node) {
85 if (node.getFirstChild() == null || AstUtil.parentOfType(node, Token.ASSIGN) != null) {
89 FunctionRecord record = getState().getCurrentFunctionRecord();
93 Node nameNode = getFunctionNameNode(record.functionNode);
94 if (nameNode == null) {
97 valueReturningFunctions.add(record);
100 private void handleThrow() {
101 FunctionRecord record = getState().getCurrentFunctionRecord();
102 if (record == null) {
105 Node nameNode = getFunctionNameNode(record.functionNode);
106 if (nameNode == null) {
109 throwingFunctions.add(record);
113 public void leaveNode(Node node) {
114 if (node.getType() != Token.FUNCTION) {
118 FunctionRecord record = getState().getCurrentFunctionRecord();
119 if (record != null) {
120 checkFunctionAnnotation(record);
124 @SuppressWarnings("unused")
125 private void checkFunctionAnnotation(FunctionRecord function) {
126 String functionName = getFunctionName(function.functionNode);
127 if (functionName == null) {
130 String[] parts = functionName.split("\\.");
131 functionName = parts[parts.length - 1];
132 boolean isApiFunction = !functionName.startsWith("_")
133 && (function.isTopLevelFunction()
134 || (function.enclosingType != null
135 && isPlainTopLevelFunction(function.enclosingFunctionRecord)));
137 boolean isReturningFunction = valueReturningFunctions.contains(function);
138 boolean isInterfaceFunction =
139 function.enclosingType != null && function.enclosingType.isInterface();
140 int invalidAnnotationIndex =
141 invalidReturnAnnotationIndex(function.info);
142 if (invalidAnnotationIndex != -1) {
143 String suggestedResolution = (isReturningFunction || isInterfaceFunction)
144 ? "should be \"@return {<type>}\""
145 : "please remove, as function does not return value";
146 getContext().reportErrorAtOffset(
147 function.info.getOriginalCommentPosition() + invalidAnnotationIndex,
149 "invalid return type annotation found - %s", suggestedResolution));
152 Node functionNameNode = getFunctionNameNode(function.functionNode);
153 if (functionNameNode == null) {
157 if (isReturningFunction) {
158 if (!function.isConstructor() && !function.hasReturnAnnotation() && isApiFunction) {
159 reportErrorAtNodeStart(
161 "@return annotation is required for API functions that return value");
164 // A @return-function that does not actually return anything and
165 // is intended to be overridden in subclasses must throw.
166 if (function.hasReturnAnnotation()
167 && !isInterfaceFunction
168 && !throwingFunctions.contains(function)) {
169 reportErrorAtNodeStart(functionNameNode,
170 "@return annotation found, yet function does not return value");
175 private static boolean isPlainTopLevelFunction(FunctionRecord record) {
176 return record != null && record.isTopLevelFunction()
177 && (record.enclosingType == null && !record.isConstructor());
180 private String getFunctionName(Node functionNode) {
181 Node nameNode = getFunctionNameNode(functionNode);
182 return nameNode == null ? null : getState().getNodeText(nameNode);
185 private static int invalidReturnAnnotationIndex(JSDocInfo info) {
189 Matcher m = INVALID_RETURN_PATTERN.matcher(info.getOriginalCommentString());
190 return m.find() ? m.start(1) : -1;
193 private static Node getFunctionNameNode(Node functionNode) {
194 // FIXME: Do not require annotation for assignment-RHS functions.
195 return AstUtil.getFunctionNameNode(functionNode);