Home | History | Annotate | Download | only in checks
      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