1package org.chromium.devtools.jsdoc.checks; 2 3import com.google.javascript.rhino.head.Token; 4import com.google.javascript.rhino.head.ast.AstNode; 5import com.google.javascript.rhino.head.ast.FunctionCall; 6import com.google.javascript.rhino.head.ast.FunctionNode; 7 8import java.util.ArrayList; 9import java.util.HashMap; 10import java.util.HashSet; 11import java.util.List; 12import java.util.Map; 13import java.util.Set; 14 15public final class FunctionReceiverChecker extends ContextTrackingChecker { 16 17 private static final Set<String> FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT = 18 new HashSet<>(); 19 private static final String SUPPRESSION_HINT = "This check can be suppressed using " 20 + "@suppressReceiverCheck annotation on function declaration."; 21 static { 22 // Array.prototype methods. 23 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("every"); 24 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("filter"); 25 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("forEach"); 26 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("map"); 27 FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("some"); 28 } 29 30 private final Map<String, FunctionRecord> nestedFunctionsByName = new HashMap<>(); 31 private final Map<String, Set<CallSite>> callSitesByFunctionName = new HashMap<>(); 32 private final Map<String, Set<SymbolicArgument>> symbolicArgumentsByName = new HashMap<>(); 33 private final Set<FunctionRecord> functionsRequiringThisAnnotation = new HashSet<>(); 34 35 @Override 36 void enterNode(AstNode node) { 37 switch (node.getType()) { 38 case Token.CALL: 39 handleCall((FunctionCall) node); 40 break; 41 case Token.FUNCTION: { 42 handleFunction((FunctionNode) node); 43 break; 44 } 45 case Token.THIS: { 46 handleThis(); 47 break; 48 } 49 default: 50 break; 51 } 52 } 53 54 private void handleCall(FunctionCall functionCall) { 55 String[] callParts = getContext().getNodeText(functionCall.getTarget()).split("\\."); 56 String firstPart = callParts[0]; 57 List<AstNode> argumentNodes = functionCall.getArguments(); 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(FunctionNode node) { 84 FunctionRecord function = getState().getCurrentFunctionRecord(); 85 if (function == null) { 86 return; 87 } 88 if (function.isTopLevelFunction()) { 89 symbolicArgumentsByName.clear(); 90 } else { 91 AstNode nameNode = AstUtil.getFunctionNameNode(node); 92 if (nameNode == null) { 93 return; 94 } 95 nestedFunctionsByName.put(getContext().getNodeText(nameNode), function); 96 } 97 } 98 99 private void handleThis() { 100 FunctionRecord function = getState().getCurrentFunctionRecord(); 101 if (function == null) { 102 return; 103 } 104 if (!function.isTopLevelFunction() && !function.isConstructor) { 105 functionsRequiringThisAnnotation.add(function); 106 } 107 } 108 109 private List<String> argumentsForCall(List<AstNode> argumentNodes) { 110 int argumentCount = argumentNodes.size(); 111 List<String> arguments = new ArrayList<>(argumentCount); 112 for (AstNode argumentNode : argumentNodes) { 113 arguments.add(getContext().getNodeText(argumentNode)); 114 } 115 return arguments; 116 } 117 118 private void saveSymbolicArguments( 119 String functionName, List<AstNode> argumentNodes, List<String> arguments) { 120 int argumentCount = arguments.size(); 121 CheckedReceiverPresence receiverPresence = CheckedReceiverPresence.MISSING; 122 if (FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.contains(functionName)) { 123 if (argumentCount >= 2) { 124 receiverPresence = CheckedReceiverPresence.PRESENT; 125 } 126 } else if ("addEventListener".equals(functionName) || 127 "removeEventListener".equals(functionName)) { 128 String receiverArgument = argumentCount < 3 ? "" : arguments.get(2); 129 switch (receiverArgument) { 130 case "": 131 case "true": 132 case "false": 133 receiverPresence = CheckedReceiverPresence.MISSING; 134 break; 135 case "this": 136 receiverPresence = CheckedReceiverPresence.PRESENT; 137 break; 138 default: 139 receiverPresence = CheckedReceiverPresence.IGNORE; 140 } 141 } 142 143 for (int i = 0; i < argumentCount; ++i) { 144 String argumentText = arguments.get(i); 145 getOrCreateSetByKey(symbolicArgumentsByName, argumentText).add( 146 new SymbolicArgument(receiverPresence, argumentNodes.get(i))); 147 } 148 } 149 150 private static <K, T> Set<T> getOrCreateSetByKey(Map<K, Set<T>> map, K key) { 151 Set<T> set = map.get(key); 152 if (set == null) { 153 set = new HashSet<>(); 154 map.put(key, set); 155 } 156 return set; 157 } 158 159 private boolean isReceiverSpecified(List<String> arguments) { 160 return arguments.size() > 0 && !"null".equals(arguments.get(0)); 161 } 162 163 @Override 164 void leaveNode(AstNode node) { 165 if (node.getType() != Token.FUNCTION) { 166 return; 167 } 168 169 ContextTrackingState state = getState(); 170 FunctionRecord function = state.getCurrentFunctionRecord(); 171 if (function == null) { 172 return; 173 } 174 checkThisAnnotation(function); 175 176 // The nested function checks are only run when leaving a top-level function. 177 if (!function.isTopLevelFunction()) { 178 return; 179 } 180 181 for (FunctionRecord record : nestedFunctionsByName.values()) { 182 processFunctionUsesAsArgument(record, symbolicArgumentsByName.get(record.name)); 183 processFunctionCallSites(record, callSitesByFunctionName.get(record.name)); 184 } 185 186 nestedFunctionsByName.clear(); 187 callSitesByFunctionName.clear(); 188 symbolicArgumentsByName.clear(); 189 } 190 191 private void checkThisAnnotation(FunctionRecord function) { 192 AstNode functionNameNode = AstUtil.getFunctionNameNode(function.functionNode); 193 if (functionNameNode == null && function.jsDocNode == null) { 194 // Do not check anonymous functions without a JSDoc. 195 return; 196 } 197 AstNode errorTargetNode = 198 functionNameNode == null ? function.jsDocNode : functionNameNode; 199 if (errorTargetNode == null) { 200 errorTargetNode = function.functionNode; 201 } 202 boolean hasThisAnnotation = hasAnnotationTag(function.jsDocNode, "this"); 203 if (hasThisAnnotation == functionReferencesThis(function)) { 204 return; 205 } 206 if (hasThisAnnotation) { 207 if (!function.isTopLevelFunction()) { 208 reportErrorAtNodeStart( 209 errorTargetNode, 210 "@this annotation found for function not referencing 'this'"); 211 } 212 return; 213 } else { 214 reportErrorAtNodeStart( 215 errorTargetNode, 216 "@this annotation is required for functions referencing 'this'"); 217 } 218 } 219 220 private boolean functionReferencesThis(FunctionRecord function) { 221 return functionsRequiringThisAnnotation.contains(function); 222 } 223 224 private void processFunctionCallSites(FunctionRecord function, Set<CallSite> callSites) { 225 if (callSites == null) { 226 return; 227 } 228 boolean functionReferencesThis = functionReferencesThis(function); 229 for (CallSite callSite : callSites) { 230 if (functionReferencesThis == callSite.hasReceiver || function.isConstructor) { 231 continue; 232 } 233 if (callSite.hasReceiver) { 234 reportErrorAtNodeStart(callSite.callNode, 235 "Receiver specified for a function not referencing 'this'"); 236 } else { 237 reportErrorAtNodeStart(callSite.callNode, 238 "Receiver not specified for a function referencing 'this'"); 239 } 240 } 241 } 242 243 private void processFunctionUsesAsArgument( 244 FunctionRecord function, Set<SymbolicArgument> argumentUses) { 245 if (argumentUses == null || 246 hasAnnotationTag(function.jsDocNode, "suppressReceiverCheck")) { 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 AstNode node; 281 282 public SymbolicArgument(CheckedReceiverPresence receiverPresence, AstNode node) { 283 this.receiverPresence = receiverPresence; 284 this.node = node; 285 } 286 } 287 288 private static class CallSite { 289 boolean hasReceiver; 290 FunctionCall callNode; 291 292 public CallSite(boolean hasReceiver, FunctionCall callNode) { 293 this.hasReceiver = hasReceiver; 294 this.callNode = callNode; 295 } 296 } 297} 298