Index: Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/FunctionReceiverChecker.java |
diff --git a/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/FunctionReceiverChecker.java b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/FunctionReceiverChecker.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8fa47757ef842a1e6afb5d1a4c4d5e33b2914b88 |
--- /dev/null |
+++ b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/FunctionReceiverChecker.java |
@@ -0,0 +1,146 @@ |
+package org.chromium.devtools.jsdoc.checks; |
+ |
+import com.google.javascript.rhino.head.Token; |
+import com.google.javascript.rhino.head.ast.AstNode; |
+import com.google.javascript.rhino.head.ast.FunctionCall; |
+import com.google.javascript.rhino.head.ast.FunctionNode; |
+ |
+import java.util.ArrayList; |
+import java.util.HashMap; |
+import java.util.HashSet; |
+import java.util.List; |
+import java.util.Map; |
+import java.util.Set; |
+ |
+public final class FunctionReceiverChecker extends ContextTrackingChecker { |
+ |
+ private final Map<String, FunctionRecord> nestedFunctionsByName = new HashMap<>(); |
+ private final Map<String, Set<CallSite>> callSitesByFunctionName = new HashMap<>(); |
+ private final Map<String, Set<AstNode>> argumentNodesByName = new HashMap<>(); |
+ |
+ @Override |
+ void enterNode(AstNode node) { |
+ switch (node.getType()) { |
+ case Token.CALL: |
+ handleCall((FunctionCall) node); |
+ break; |
+ case Token.FUNCTION: |
+ FunctionRecord function = getState().getCurrentFunctionRecord(); |
+ if (function == null) { |
+ break; |
+ } |
+ if (function.isTopLevelFunction()) { |
+ argumentNodesByName.clear(); |
+ } else { |
+ AstNode nameNode = AstUtil.getFunctionNameNode((FunctionNode) node); |
+ if (nameNode == null) { |
+ break; |
+ } |
+ nestedFunctionsByName.put(getContext().getNodeText(nameNode), function); |
+ } |
+ break; |
+ default: |
+ break; |
+ } |
+ } |
+ |
+ private void handleCall(FunctionCall functionCall) { |
+ String[] targetParts = getContext().getNodeText(functionCall.getTarget()).split("\\."); |
+ String firstPart = targetParts[0]; |
+ int partCount = targetParts.length; |
+ List<String> arguments = new ArrayList<>(functionCall.getArguments().size()); |
+ for (AstNode argumentNode : functionCall.getArguments()) { |
+ String argumentText = getContext().getNodeText(argumentNode); |
+ arguments.add(argumentText); |
+ getOrCreateSetByKey(argumentNodesByName, argumentText).add(argumentNode); |
+ } |
+ boolean hasBind = partCount > 1 && "bind".equals(targetParts[partCount - 1]); |
+ if (hasBind && partCount == 3 && "this".equals(firstPart) && |
+ !(arguments.size() > 0 && "this".equals(arguments.get(0)))) { |
+ reportErrorAtNodeStart(functionCall, |
+ "Member function can only be bound to 'this' as the receiver"); |
+ return; |
+ } |
+ if (partCount > 2 || "this".equals(firstPart)) { |
+ return; |
+ } |
+ boolean hasReceiver = hasBind && isReceiverSpecified(arguments); |
+ hasReceiver |= (partCount == 2) && |
+ ("call".equals(targetParts[1]) || "apply".equals(targetParts[1])) && |
+ isReceiverSpecified(arguments); |
+ getOrCreateSetByKey(callSitesByFunctionName, firstPart) |
+ .add(new CallSite(hasReceiver, functionCall)); |
+ } |
+ |
+ private static <K, T> Set<T> getOrCreateSetByKey(Map<K, Set<T>> map, K key) { |
+ Set<T> set = map.get(key); |
+ if (set == null) { |
+ set = new HashSet<>(); |
+ map.put(key, set); |
+ } |
+ return set; |
+ } |
+ |
+ private boolean isReceiverSpecified(List<String> arguments) { |
+ return arguments.size() > 0 && !"null".equals(arguments.get(0)); |
+ } |
+ |
+ @Override |
+ void leaveNode(AstNode node) { |
+ if (node.getType() != Token.FUNCTION) { |
+ return; |
+ } |
+ |
+ FunctionRecord function = getState().getCurrentFunctionRecord(); |
+ if (function == null || !function.isTopLevelFunction()) { |
+ return; |
+ } |
+ |
+ for (FunctionRecord record : nestedFunctionsByName.values()) { |
+ processNestedFunction(record); |
+ } |
+ nestedFunctionsByName.clear(); |
+ callSitesByFunctionName.clear(); |
+ argumentNodesByName.clear(); |
+ } |
+ |
+ private void processNestedFunction(FunctionRecord record) { |
+ String name = record.name; |
+ Set<AstNode> argumentUsages = argumentNodesByName.get(name); |
+ Set<CallSite> callSites = callSitesByFunctionName.get(name); |
+ boolean hasThisAnnotation = AstUtil.hasThisAnnotation(record.functionNode, getContext()); |
+ if (hasThisAnnotation && argumentUsages != null) { |
+ for (AstNode argumentNode : argumentUsages) { |
+ reportErrorAtNodeStart(argumentNode, |
+ "Function annotated with @this used as argument without " + |
+ "bind(<non-null-receiver>)"); |
+ } |
+ } |
+ |
+ if (callSites == null) { |
+ return; |
+ } |
+ for (CallSite callSite : callSites) { |
+ if (hasThisAnnotation == callSite.hasReceiver || record.isConstructor) { |
+ continue; |
+ } |
+ if (callSite.hasReceiver) { |
+ reportErrorAtNodeStart(callSite.callNode, |
+ "Receiver specified for a function not annotated with @this"); |
+ } else { |
+ reportErrorAtNodeStart(callSite.callNode, |
+ "Receiver not specified for a function annotated with @this"); |
+ } |
+ } |
+ } |
+ |
+ private static class CallSite { |
+ boolean hasReceiver; |
+ FunctionCall callNode; |
+ |
+ public CallSite(boolean hasReceiver, FunctionCall callNode) { |
+ this.hasReceiver = hasReceiver; |
+ this.callNode = callNode; |
+ } |
+ } |
+} |