Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / scripts / jsdoc-validator / src / org / chromium / devtools / jsdoc / checks / FunctionReceiverChecker.java
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 }