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 |
index 8fa47757ef842a1e6afb5d1a4c4d5e33b2914b88..85cb9ad9e40b26f7cd4e6a1fe529048ee09190dc 100644 |
--- 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 |
@@ -14,9 +14,22 @@ import java.util.Set; |
public final class FunctionReceiverChecker extends ContextTrackingChecker { |
+ private static final Set<String> FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT = |
+ new HashSet<>(); |
+ private static final String SUPPRESSION_HINT = "This check can be suppressed using " |
+ + "@suppressReceiverCheck annotation on function declaration."; |
+ static { |
+ // Array.prototype methods. |
+ FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("every"); |
+ FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("filter"); |
+ FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("forEach"); |
+ FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("map"); |
+ FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.add("some"); |
+ } |
+ |
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<>(); |
+ private final Map<String, Set<SymbolicArgument>> symbolicArgumentsByName = new HashMap<>(); |
@Override |
void enterNode(AstNode node) { |
@@ -30,7 +43,7 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker { |
break; |
} |
if (function.isTopLevelFunction()) { |
- argumentNodesByName.clear(); |
+ symbolicArgumentsByName.clear(); |
} else { |
AstNode nameNode = AstUtil.getFunctionNameNode((FunctionNode) node); |
if (nameNode == null) { |
@@ -45,18 +58,18 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker { |
} |
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)))) { |
+ String[] callParts = getContext().getNodeText(functionCall.getTarget()).split("\\."); |
+ String firstPart = callParts[0]; |
+ List<AstNode> argumentNodes = functionCall.getArguments(); |
+ List<String> actualArguments = argumentsForCall(argumentNodes); |
+ int partCount = callParts.length; |
+ String functionName = callParts[partCount - 1]; |
+ |
+ saveSymbolicArguments(functionName, argumentNodes, actualArguments); |
+ |
+ boolean isBindCall = partCount > 1 && "bind".equals(functionName); |
+ if (isBindCall && partCount == 3 && "this".equals(firstPart) && |
+ !(actualArguments.size() > 0 && "this".equals(actualArguments.get(0)))) { |
reportErrorAtNodeStart(functionCall, |
"Member function can only be bound to 'this' as the receiver"); |
return; |
@@ -64,14 +77,55 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker { |
if (partCount > 2 || "this".equals(firstPart)) { |
return; |
} |
- boolean hasReceiver = hasBind && isReceiverSpecified(arguments); |
+ boolean hasReceiver = isBindCall && isReceiverSpecified(actualArguments); |
hasReceiver |= (partCount == 2) && |
- ("call".equals(targetParts[1]) || "apply".equals(targetParts[1])) && |
- isReceiverSpecified(arguments); |
+ ("call".equals(functionName) || "apply".equals(functionName)) && |
+ isReceiverSpecified(actualArguments); |
getOrCreateSetByKey(callSitesByFunctionName, firstPart) |
.add(new CallSite(hasReceiver, functionCall)); |
} |
+ private List<String> argumentsForCall(List<AstNode> argumentNodes) { |
+ int argumentCount = argumentNodes.size(); |
+ List<String> arguments = new ArrayList<>(argumentCount); |
+ for (AstNode argumentNode : argumentNodes) { |
+ arguments.add(getContext().getNodeText(argumentNode)); |
+ } |
+ return arguments; |
+ } |
+ |
+ private void saveSymbolicArguments( |
+ String functionName, List<AstNode> argumentNodes, List<String> arguments) { |
+ int argumentCount = arguments.size(); |
+ CheckedReceiverPresence receiverPresence = CheckedReceiverPresence.MISSING; |
+ if (FUNCTIONS_WITH_CALLBACK_RECEIVER_AS_SECOND_ARGUMENT.contains(functionName)) { |
+ if (argumentCount >= 2) { |
+ receiverPresence = CheckedReceiverPresence.PRESENT; |
+ } |
+ } else if ("addEventListener".equals(functionName) || |
+ "removeEventListener".equals(functionName)) { |
+ String receiverArgument = argumentCount < 3 ? "" : arguments.get(2); |
+ switch (receiverArgument) { |
+ case "": |
+ case "true": |
+ case "false": |
+ receiverPresence = CheckedReceiverPresence.MISSING; |
+ break; |
+ case "this": |
+ receiverPresence = CheckedReceiverPresence.PRESENT; |
+ break; |
+ default: |
+ receiverPresence = CheckedReceiverPresence.IGNORE; |
+ } |
+ } |
+ |
+ for (int i = 0; i < argumentCount; ++i) { |
+ String argumentText = arguments.get(i); |
+ getOrCreateSetByKey(symbolicArgumentsByName, argumentText).add( |
+ new SymbolicArgument(receiverPresence, argumentNodes.get(i))); |
+ } |
+ } |
+ |
private static <K, T> Set<T> getOrCreateSetByKey(Map<K, Set<T>> map, K key) { |
Set<T> set = map.get(key); |
if (set == null) { |
@@ -97,31 +151,22 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker { |
} |
for (FunctionRecord record : nestedFunctionsByName.values()) { |
- processNestedFunction(record); |
+ processFunctionUsesAsArgument(record, symbolicArgumentsByName.get(record.name)); |
+ processFunctionCallSites(record, callSitesByFunctionName.get(record.name)); |
} |
+ |
nestedFunctionsByName.clear(); |
callSitesByFunctionName.clear(); |
- argumentNodesByName.clear(); |
+ symbolicArgumentsByName.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>)"); |
- } |
- } |
- |
+ private void processFunctionCallSites(FunctionRecord function, Set<CallSite> callSites) { |
if (callSites == null) { |
return; |
} |
+ boolean hasThisAnnotation = hasAnnotationTag(function.functionNode, "this"); |
for (CallSite callSite : callSites) { |
- if (hasThisAnnotation == callSite.hasReceiver || record.isConstructor) { |
+ if (hasThisAnnotation == callSite.hasReceiver || function.isConstructor) { |
continue; |
} |
if (callSite.hasReceiver) { |
@@ -134,6 +179,51 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker { |
} |
} |
+ private void processFunctionUsesAsArgument( |
+ FunctionRecord function, Set<SymbolicArgument> argumentUses) { |
+ if (argumentUses == null || |
+ hasAnnotationTag(function.functionNode, "suppressReceiverCheck")) { |
+ return; |
+ } |
+ |
+ boolean hasThisAnnotation = hasAnnotationTag(function.functionNode, "this"); |
+ for (SymbolicArgument argument : argumentUses) { |
+ if (argument.receiverPresence == CheckedReceiverPresence.IGNORE) { |
+ continue; |
+ } |
+ boolean receiverProvided = |
+ argument.receiverPresence == CheckedReceiverPresence.PRESENT; |
+ if (hasThisAnnotation == receiverProvided) { |
+ continue; |
+ } |
+ if (hasThisAnnotation) { |
+ reportErrorAtNodeStart(argument.node, |
+ "Function annotated with @this used as argument without " + |
+ "a receiver. " + SUPPRESSION_HINT); |
+ } else { |
+ reportErrorAtNodeStart(argument.node, |
+ "Function not annotated with @this used as argument with " + |
+ "a receiver. " + SUPPRESSION_HINT); |
+ } |
+ } |
+ } |
+ |
+ private static enum CheckedReceiverPresence { |
+ PRESENT, |
+ MISSING, |
+ IGNORE |
+ } |
+ |
+ private static class SymbolicArgument { |
+ CheckedReceiverPresence receiverPresence; |
+ AstNode node; |
+ |
+ public SymbolicArgument(CheckedReceiverPresence receiverPresence, AstNode node) { |
+ this.receiverPresence = receiverPresence; |
+ this.node = node; |
+ } |
+ } |
+ |
private static class CallSite { |
boolean hasReceiver; |
FunctionCall callNode; |