1 package org.chromium.devtools.jsdoc.checks;
3 import com.google.common.base.Preconditions;
4 import com.google.javascript.rhino.Node;
5 import com.google.javascript.rhino.Token;
7 import java.util.ArrayList;
8 import java.util.HashMap;
9 import java.util.HashSet;
10 import java.util.List;
14 public final class FunctionReceiverChecker extends ContextTrackingChecker {
16 private static final Set<String> FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT =
18 private static final String SUPPRESSION_HINT = "This check can be suppressed using "
19 + "@suppressReceiverCheck annotation on function declaration.";
21 // Array.prototype methods.
22 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("every");
23 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("filter");
24 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("forEach");
25 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("map");
26 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("some");
29 private final Map<String, FunctionRecord> nestedFunctionsByName = new HashMap<>();
30 private final Map<String, Set<CallSite>> callSitesByFunctionName = new HashMap<>();
31 private final Map<String, Set<SymbolicArgument>> symbolicArgumentsByName = new HashMap<>();
32 private final Set<FunctionRecord> functionsRequiringThisAnnotation = new HashSet<>();
35 void enterNode(Node node) {
36 switch (node.getType()) {
40 case Token.FUNCTION: {
53 private void handleCall(Node functionCall) {
54 Preconditions.checkState(functionCall.isCall());
55 String[] callParts = getContext().getNodeText(functionCall.getFirstChild()).split("\\.");
56 String firstPart = callParts[0];
57 List<Node> argumentNodes = AstUtil.getArguments(functionCall);
58 List<String> actualArguments = argumentsForCall(argumentNodes);
59 int partCount = callParts.length;
60 String functionName = callParts[partCount - 1];
62 saveSymbolicArguments(functionName, argumentNodes, actualArguments);
64 boolean isBindCall = partCount > 1 && "bind".equals(functionName);
65 if (isBindCall && partCount == 3 && "this".equals(firstPart) &&
66 !(actualArguments.size() > 0 && "this".equals(actualArguments.get(0)))) {
67 reportErrorAtNodeStart(functionCall,
68 "Member function can only be bound to 'this' as the receiver");
71 if (partCount > 2 || "this".equals(firstPart)) {
74 boolean hasReceiver = isBindCall && isReceiverSpecified(actualArguments);
75 hasReceiver |= (partCount == 2) &&
76 ("call".equals(functionName) || "apply".equals(functionName)) &&
77 isReceiverSpecified(actualArguments);
78 getOrCreateSetByKey(callSitesByFunctionName, firstPart)
79 .add(new CallSite(hasReceiver, functionCall));
83 private void handleFunction(Node node) {
84 Preconditions.checkState(node.isFunction());
85 FunctionRecord function = getState().getCurrentFunctionRecord();
86 if (function == null) {
89 if (function.isTopLevelFunction()) {
90 symbolicArgumentsByName.clear();
92 Node nameNode = AstUtil.getFunctionNameNode(node);
93 if (nameNode == null) {
96 nestedFunctionsByName.put(getContext().getNodeText(nameNode), function);
100 private void handleThis() {
101 FunctionRecord function = getState().getCurrentFunctionRecord();
102 if (function == null) {
105 if (!function.isTopLevelFunction() && !function.isConstructor()) {
106 functionsRequiringThisAnnotation.add(function);
110 private List<String> argumentsForCall(List<Node> argumentNodes) {
111 int argumentCount = argumentNodes.size();
112 List<String> arguments = new ArrayList<>(argumentCount);
113 for (Node argumentNode : argumentNodes) {
114 arguments.add(getContext().getNodeText(argumentNode));
119 private void saveSymbolicArguments(
120 String functionName, List<Node> argumentNodes, List<String> arguments) {
121 int argumentCount = arguments.size();
122 CheckedReceiverPresence receiverPresence = CheckedReceiverPresence.MISSING;
123 if (FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.contains(functionName)) {
124 if (argumentCount >= 2) {
125 receiverPresence = CheckedReceiverPresence.PRESENT;
127 } else if ("addEventListener".equals(functionName) ||
128 "removeEventListener".equals(functionName)) {
129 String receiverArgument = argumentCount < 3 ? "" : arguments.get(2);
130 switch (receiverArgument) {
134 receiverPresence = CheckedReceiverPresence.MISSING;
137 receiverPresence = CheckedReceiverPresence.PRESENT;
140 receiverPresence = CheckedReceiverPresence.IGNORE;
144 for (int i = 0; i < argumentCount; ++i) {
145 String argumentText = arguments.get(i);
146 getOrCreateSetByKey(symbolicArgumentsByName, argumentText).add(
147 new SymbolicArgument(receiverPresence, argumentNodes.get(i)));
151 private static <K, T> Set<T> getOrCreateSetByKey(Map<K, Set<T>> map, K key) {
152 Set<T> set = map.get(key);
154 set = new HashSet<>();
160 private boolean isReceiverSpecified(List<String> arguments) {
161 return arguments.size() > 0 && !"null".equals(arguments.get(0));
165 void leaveNode(Node node) {
166 if (node.getType() != Token.FUNCTION) {
170 ContextTrackingState state = getState();
171 FunctionRecord function = state.getCurrentFunctionRecord();
172 if (function == null) {
175 checkThisAnnotation(function);
177 // The nested function checks are only run when leaving a top-level function.
178 if (!function.isTopLevelFunction()) {
182 for (FunctionRecord record : nestedFunctionsByName.values()) {
183 processFunctionUsesAsArgument(record, symbolicArgumentsByName.get(record.name));
184 processFunctionCallSites(record, callSitesByFunctionName.get(record.name));
187 nestedFunctionsByName.clear();
188 callSitesByFunctionName.clear();
189 symbolicArgumentsByName.clear();
192 private void checkThisAnnotation(FunctionRecord function) {
193 Node functionNameNode = AstUtil.getFunctionNameNode(function.functionNode);
194 if (functionNameNode == null && function.info == null) {
195 // Do not check anonymous functions without a JSDoc.
198 int errorTargetOffset = functionNameNode == null
199 ? (function.info == null
200 ? function.functionNode.getSourceOffset()
201 : function.info.getOriginalCommentPosition())
202 : functionNameNode.getSourceOffset();
203 boolean hasThisAnnotation = function.hasThisAnnotation();
204 if (hasThisAnnotation == functionReferencesThis(function)) {
207 if (hasThisAnnotation) {
208 if (!function.isTopLevelFunction()) {
211 "@this annotation found for function not referencing 'this'");
217 "@this annotation is required for functions referencing 'this'");
221 private boolean functionReferencesThis(FunctionRecord function) {
222 return functionsRequiringThisAnnotation.contains(function);
225 private void processFunctionCallSites(FunctionRecord function, Set<CallSite> callSites) {
226 if (callSites == null) {
229 boolean functionReferencesThis = functionReferencesThis(function);
230 for (CallSite callSite : callSites) {
231 if (functionReferencesThis == callSite.hasReceiver || function.isConstructor()) {
234 if (callSite.hasReceiver) {
235 reportErrorAtNodeStart(callSite.callNode,
236 "Receiver specified for a function not referencing 'this'");
238 reportErrorAtNodeStart(callSite.callNode,
239 "Receiver not specified for a function referencing 'this'");
244 private void processFunctionUsesAsArgument(
245 FunctionRecord function, Set<SymbolicArgument> argumentUses) {
246 if (argumentUses == null || function.suppressesReceiverCheck()) {
250 boolean referencesThis = functionReferencesThis(function);
251 for (SymbolicArgument argument : argumentUses) {
252 if (argument.receiverPresence == CheckedReceiverPresence.IGNORE) {
255 boolean receiverProvided =
256 argument.receiverPresence == CheckedReceiverPresence.PRESENT;
257 if (referencesThis == receiverProvided) {
260 if (referencesThis) {
261 reportErrorAtNodeStart(argument.node,
262 "Function referencing 'this' used as argument without " +
263 "a receiver. " + SUPPRESSION_HINT);
265 reportErrorAtNodeStart(argument.node,
266 "Function not referencing 'this' used as argument with " +
267 "a receiver. " + SUPPRESSION_HINT);
272 private static enum CheckedReceiverPresence {
278 private static class SymbolicArgument {
279 CheckedReceiverPresence receiverPresence;
282 public SymbolicArgument(CheckedReceiverPresence receiverPresence, Node node) {
283 this.receiverPresence = receiverPresence;
288 private static class CallSite {
292 public CallSite(boolean hasReceiver, Node callNode) {
293 this.hasReceiver = hasReceiver;
294 this.callNode = callNode;