1 package org.chromium.devtools.jsdoc.checks; 2 3 import com.google.common.base.Preconditions; 4 import com.google.javascript.rhino.Node; 5 import com.google.javascript.rhino.Token; 6 7 import java.util.ArrayList; 8 import java.util.HashMap; 9 import java.util.HashSet; 10 import java.util.List; 11 import java.util.Map; 12 import java.util.Set; 13 14 public final class FunctionReceiverChecker extends ContextTrackingChecker { 15 16 private static final Set<String> FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT = 17 new HashSet<>(); 18 private static final String SUPPRESSION_HINT = "This check can be suppressed using " 19 + "@suppressReceiverCheck annotation on function declaration."; 20 static { 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"); 27 } 28 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<>(); 33 34 @Override 35 void enterNode(Node node) { 36 switch (node.getType()) { 37 case Token.CALL: 38 handleCall(node); 39 break; 40 case Token.FUNCTION: { 41 handleFunction(node); 42 break; 43 } 44 case Token.THIS: { 45 handleThis(); 46 break; 47 } 48 default: 49 break; 50 } 51 } 52 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]; 61 62 saveSymbolicArguments(functionName, argumentNodes, actualArguments); 63 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"); 69 return; 70 } 71 if (partCount > 2 || "this".equals(firstPart)) { 72 return; 73 } 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)); 80 } 81 82 83 private void handleFunction(Node node) { 84 Preconditions.checkState(node.isFunction()); 85 FunctionRecord function = getState().getCurrentFunctionRecord(); 86 if (function == null) { 87 return; 88 } 89 if (function.isTopLevelFunction()) { 90 symbolicArgumentsByName.clear(); 91 } else { 92 Node nameNode = AstUtil.getFunctionNameNode(node); 93 if (nameNode == null) { 94 return; 95 } 96 nestedFunctionsByName.put(getContext().getNodeText(nameNode), function); 97 } 98 } 99 100 private void handleThis() { 101 FunctionRecord function = getState().getCurrentFunctionRecord(); 102 if (function == null) { 103 return; 104 } 105 if (!function.isTopLevelFunction() && !function.isConstructor()) { 106 functionsRequiringThisAnnotation.add(function); 107 } 108 } 109 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)); 115 } 116 return arguments; 117 } 118 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; 126 } 127 } else if ("addEventListener".equals(functionName) || 128 "removeEventListener".equals(functionName)) { 129 String receiverArgument = argumentCount < 3 ? "" : arguments.get(2); 130 switch (receiverArgument) { 131 case "": 132 case "true": 133 case "false": 134 receiverPresence = CheckedReceiverPresence.MISSING; 135 break; 136 case "this": 137 receiverPresence = CheckedReceiverPresence.PRESENT; 138 break; 139 default: 140 receiverPresence = CheckedReceiverPresence.IGNORE; 141 } 142 } 143 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))); 148 } 149 } 150 151 private static <K, T> Set<T> getOrCreateSetByKey(Map<K, Set<T>> map, K key) { 152 Set<T> set = map.get(key); 153 if (set == null) { 154 set = new HashSet<>(); 155 map.put(key, set); 156 } 157 return set; 158 } 159 160 private boolean isReceiverSpecified(List<String> arguments) { 161 return arguments.size() > 0 && !"null".equals(arguments.get(0)); 162 } 163 164 @Override 165 void leaveNode(Node node) { 166 if (node.getType() != Token.FUNCTION) { 167 return; 168 } 169 170 ContextTrackingState state = getState(); 171 FunctionRecord function = state.getCurrentFunctionRecord(); 172 if (function == null) { 173 return; 174 } 175 checkThisAnnotation(function); 176 177 // The nested function checks are only run when leaving a top-level function. 178 if (!function.isTopLevelFunction()) { 179 return; 180 } 181 182 for (FunctionRecord record : nestedFunctionsByName.values()) { 183 processFunctionUsesAsArgument(record, symbolicArgumentsByName.get(record.name)); 184 processFunctionCallSites(record, callSitesByFunctionName.get(record.name)); 185 } 186 187 nestedFunctionsByName.clear(); 188 callSitesByFunctionName.clear(); 189 symbolicArgumentsByName.clear(); 190 } 191 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. 196 return; 197 } 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)) { 205 return; 206 } 207 if (hasThisAnnotation) { 208 if (!function.isTopLevelFunction()) { 209 reportErrorAtOffset( 210 errorTargetOffset, 211 "@this annotation found for function not referencing 'this'"); 212 } 213 return; 214 } else { 215 reportErrorAtOffset( 216 errorTargetOffset, 217 "@this annotation is required for functions referencing 'this'"); 218 } 219 } 220 221 private boolean functionReferencesThis(FunctionRecord function) { 222 return functionsRequiringThisAnnotation.contains(function); 223 } 224 225 private void processFunctionCallSites(FunctionRecord function, Set<CallSite> callSites) { 226 if (callSites == null) { 227 return; 228 } 229 boolean functionReferencesThis = functionReferencesThis(function); 230 for (CallSite callSite : callSites) { 231 if (functionReferencesThis == callSite.hasReceiver || function.isConstructor()) { 232 continue; 233 } 234 if (callSite.hasReceiver) { 235 reportErrorAtNodeStart(callSite.callNode, 236 "Receiver specified for a function not referencing 'this'"); 237 } else { 238 reportErrorAtNodeStart(callSite.callNode, 239 "Receiver not specified for a function referencing 'this'"); 240 } 241 } 242 } 243 244 private void processFunctionUsesAsArgument( 245 FunctionRecord function, Set<SymbolicArgument> argumentUses) { 246 if (argumentUses == null || function.suppressesReceiverCheck()) { 247 return; 248 } 249 250 boolean referencesThis = functionReferencesThis(function); 251 for (SymbolicArgument argument : argumentUses) { 252 if (argument.receiverPresence == CheckedReceiverPresence.IGNORE) { 253 continue; 254 } 255 boolean receiverProvided = 256 argument.receiverPresence == CheckedReceiverPresence.PRESENT; 257 if (referencesThis == receiverProvided) { 258 continue; 259 } 260 if (referencesThis) { 261 reportErrorAtNodeStart(argument.node, 262 "Function referencing 'this' used as argument without " + 263 "a receiver. " + SUPPRESSION_HINT); 264 } else { 265 reportErrorAtNodeStart(argument.node, 266 "Function not referencing 'this' used as argument with " + 267 "a receiver. " + SUPPRESSION_HINT); 268 } 269 } 270 } 271 272 private static enum CheckedReceiverPresence { 273 PRESENT, 274 MISSING, 275 IGNORE 276 } 277 278 private static class SymbolicArgument { 279 CheckedReceiverPresence receiverPresence; 280 Node node; 281 282 public SymbolicArgument(CheckedReceiverPresence receiverPresence, Node node) { 283 this.receiverPresence = receiverPresence; 284 this.node = node; 285 } 286 } 287 288 private static class CallSite { 289 boolean hasReceiver; 290 Node callNode; 291 292 public CallSite(boolean hasReceiver, Node callNode) { 293 this.hasReceiver = hasReceiver; 294 this.callNode = callNode; 295 } 296 } 297 } 298