All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.CheckEventfulObjectDisposal Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * Copyright 2012 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.javascript.jscomp.CompilerOptions.DisposalCheckingPolicy;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TypeIRegistry;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.ObjectType;
import com.google.javascript.rhino.jstype.UnionType;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

/**
 * Check to ensure there exists a path to dispose of each eventful object
 * created.
 *
 * 

An eventful class is any class that derives from goog.events.EventHandler * or (in aggressive mode) is disposable and disposes of an eventful class when * it is disposed (see http://research.google.com/pubs/pub40738.html). * *

This pass is heuristic based and should not be used for any check * of pass/fail testing. The pass traverses the AST and marks as errors * cases where an eventful object is allocated but a dispose call is not found. * It only tracks eventful objects that has a easily identifiable static name, * i.e., objects assigned to arrays, returned from functions or captured in * closures are not considered. It simply tries to see if there exists a call to * a dispose method in the AST for every object seen as eventful. * *

This compiler pass uses the inferred types and hence either type checking or * type inference needs to be enabled. * * */ // TODO(tbreisacher): Find out if this pass is still actually useful. Delete it if not. // TODO(user): Pass needs to be updated for listenable interfaces. public final class CheckEventfulObjectDisposal implements CompilerPass { static final DiagnosticType EVENTFUL_OBJECT_NOT_DISPOSED = DiagnosticType.error( "JSC_EVENTFUL_OBJECT_NOT_DISPOSED", "eventful object created should be\n" + " * registered as disposable, or\n" + " * explicitly disposed of"); static final DiagnosticType EVENTFUL_OBJECT_PURELY_LOCAL = DiagnosticType.error( "JSC_EVENTFUL_OBJECT_PURELY_LOCAL", "a purely local eventful object cannot be disposed of later"); static final DiagnosticType OVERWRITE_PRIVATE_EVENTFUL_OBJECT = DiagnosticType.error( "JSC_OVERWRITE_PRIVATE_EVENTFUL_OBJECT", "private eventful object overwritten in subclass cannot be properly " + "disposed of"); static final DiagnosticType UNLISTEN_WITH_ANONBOUND = DiagnosticType.error( "JSC_UNLISTEN_WITH_ANONBOUND", "an unlisten call with an anonymous or bound function does not result " + "in the event being unlisted to"); // Seed types private static final String DISPOSABLE_INTERFACE_TYPE_NAME = "goog.disposable.IDisposable"; private static final String EVENT_HANDLER_TYPE_NAME = "goog.events.EventHandler"; private JSType googDisposableInterfaceType; private JSType googEventsEventHandlerType; // Eventful types private Set eventfulTypes; /* * Dispose methods is a map of types to maps from property/function name * to argument disposed/all arguments disposed. The key is used to filter * the dispose calls checked against. That is, the pass considers all dispose * calls of classes a class is derived from and not merely those in the map * of its given type. * Note: it is assumed that at most one string match will occur per * disposeMethod call. */ private Map>> disposeCalls; /** * Constant used to signify all arguments of method/function * should be marked as disposed. */ public static final int DISPOSE_ALL = -1; /** * Constant used to signify that object on which this method is called, * will itself get disposed of. */ public static final int DISPOSE_SELF = -2; private final AbstractCompiler compiler; private final TypeIRegistry typeRegistry; // At the moment only ALLOCATED and POSSIBLY_DISPOSED are used private enum SeenType { ALLOCATED, ALLOCATED_LOCALLY, POSSIBLY_DISPOSED, DISPOSED } // Combine the state and allocation site of eventful objects private static class EventfulObjectState { public SeenType seen; public Node allocationSite; } /* * The disposal checking policy used. */ private final DisposalCheckingPolicy checkingPolicy; /* * Eventize DAG represented using adjacency lists. */ private SetMultimap eventizes; /* * Maps from eventful object name to state. */ private static Map eventfulObjectMap; public CheckEventfulObjectDisposal(AbstractCompiler compiler, DisposalCheckingPolicy checkingPolicy) { this.compiler = compiler; this.checkingPolicy = checkingPolicy; this.typeRegistry = compiler.getTypeIRegistry(); this.initializeDisposeMethodsMap(); } /** * Add a new call that is used to dispose an JS object. * @param functionOrMethodName The name or suffix of a function or method * that disposes of/registers an object as disposable * @param argumentsThatAreDisposed An array of integers (ideally sorted) that * specifies the arguments of the function being disposed */ private void addDisposeCall(String functionOrMethodName, List argumentsThatAreDisposed) { String potentiallyTypeName; String propertyName; JSType objectType = null; int lastPeriod = functionOrMethodName.lastIndexOf('.'); // If function call has a period it is potentially a method function. if (lastPeriod >= 0) { potentiallyTypeName = functionOrMethodName.substring(0, lastPeriod). replaceFirst(".prototype$", ""); propertyName = functionOrMethodName.substring(lastPeriod); objectType = this.typeRegistry.getType(potentiallyTypeName); } else { propertyName = functionOrMethodName; } // Find or create property map for object type Map> map = this.disposeCalls.get(objectType); if (map == null) { map = new HashMap<>(); this.disposeCalls.put(objectType, map); } /* * If this is a static function call store the full function name, * else only the method of the object. */ if (objectType == null) { map.put(functionOrMethodName, argumentsThatAreDisposed); } else { map.put(propertyName, argumentsThatAreDisposed); } } /* * Initialize disposeMethods map with calls to dispose calls. */ private void initializeDisposeMethodsMap() { this.disposeCalls = new HashMap<>(); /* * Initialize dispose calls map. Checks for: * - Y.registerDisposable(X) * (Y has to be of type goog.Disposable) * - X.dispose() * - goog.dispose(X) * - goog.disposeAll(X...) * - X.removeAll() (X is of type goog.events.EventHandler) * - goog.array.extend(_, X...) * - Y.add(X...) or Y.push(X) */ this.addDisposeCall("goog.array.extend", ImmutableList.of(DISPOSE_ALL)); this.addDisposeCall("goog.dispose", ImmutableList.of(0)); this.addDisposeCall("goog.Disposable.registerDisposable", ImmutableList.of(0)); this.addDisposeCall("goog.disposeAll", ImmutableList.of(DISPOSE_ALL)); this.addDisposeCall("goog.events.EventHandler.removeAll", ImmutableList.of(DISPOSE_SELF)); this.addDisposeCall(".dispose", ImmutableList.of(DISPOSE_SELF)); this.addDisposeCall(".push", ImmutableList.of(0)); this.addDisposeCall(".add", ImmutableList.of(DISPOSE_SELF)); } private static Node getBase(Node n) { Node base = n; while (base.isGetProp()) { base = base.getFirstChild(); } return base; } /* * Get the type of the this in the current scope of traversal */ private static JSType getTypeOfThisForScope(NodeTraversal t) { JSType typeOfThis = t.getScopeRoot().getJSType(); if (typeOfThis == null) { return null; } ObjectType objectType = ObjectType.cast(dereference(typeOfThis)); return objectType.getTypeOfThis(); } /** * Determines if thisType is possibly a subtype of thatType. * *

It differs from isSubtype only in that thisType gets expanded * if it is a union. * *

Common case targeted is a function returning an eventful object * that may also return a null. * * @param thisType the JSType being tested * @param thatType the JSType that is possibly a base of thisType * @return whether thisType is possibly subtype of thatType */ private static boolean isPossiblySubtype(JSType thisType, JSType thatType) { if (thisType == null) { return false; } JSType type = thisType; if (type.isUnionType()) { for (JSType alternate : type.toMaybeUnionType().getAlternates()) { if (alternate.isSubtype(thatType)) { return true; } } } else { if (type.isSubtype(thatType)) { return true; } } return false; } private static JSType dereference(JSType type) { return type == null ? null : type.dereference(); } /* * Create a unique identification string for Node n, or null if function * called with invalid argument. * * This function is basically used to distinguish between: * A.B = function() { * this.eh = new ... * } * and * C.D = function() { * this.eh = new ... * } * * As well as * A.B = function() { * var eh = new ... * } * and * C.D = function() { * var eh = new ... * } * * Warning: Inheritance is not currently handled. */ private static String generateKey(NodeTraversal t, Node n, boolean noLocalVariables) { if (n == null) { return null; } String key; Node scopeNode = t.getScopeRoot(); if (n.isName()) { if (noLocalVariables) { return null; } key = n.getQualifiedName(); if (scopeNode.isFunction()) { JSType parentScopeType = t.getTypedScope().getParentScope().getTypeOfThis(); /* * If the locally defined variable is defined within a function, use * the function name to create ID. */ if (!parentScopeType.isGlobalThisType()) { key = parentScopeType + "~" + key; } key = NodeUtil.getName(scopeNode) + "=" + key; } } else { /* * Only handle cases such as a.b.c.X and not cases where the * eventful object is stored in an array or uses a function to * determine the index. * * Note: Inheritance changes the name that should be returned here */ if (!n.isQualifiedName()) { return null; } key = n.getQualifiedName(); /* * If it is not a simple variable and doesn't use this, then we assume * global variable. */ Node base = getBase(n); if (base != null && base.isThis()) { if (base.getJSType().isUnknownType()) { // Handle anonymous function created in constructor: // // /** // * @extends {goog.SubDisposable} // * @constructor */ // speel.Person = function() { // this.run = function() { // this.eh = new goog.events.EventHandler(); // } //}; key = t.getTypedScope().getParentScope().getTypeOfThis() + "~" + key; } else { if (n.getFirstChild() == null) { key = base.getJSType() + "=" + key; } else { ObjectType objectType = ObjectType.cast(dereference(n.getFirstChild().getJSType())); if (objectType == null) { return null; } ObjectType hObjT = objectType; String propertyName = n.getLastChild().getString(); while (objectType != null) { hObjT = objectType; objectType = objectType.getImplicitPrototype(); if (objectType == null) { break; } if (objectType.getDisplayName().endsWith("prototype")) { continue; } if (!objectType.getPropertyNames().contains(propertyName)) { break; } } key = hObjT + "=" + key; } } } } return key; } @Override public void process(Node externs, Node root) { // This pass should not have gotten added in this case Preconditions.checkArgument(checkingPolicy != DisposalCheckingPolicy.OFF); // Initialize types googDisposableInterfaceType = this.typeRegistry.getType(DISPOSABLE_INTERFACE_TYPE_NAME); googEventsEventHandlerType = this.typeRegistry.getType(EVENT_HANDLER_TYPE_NAME); /* * Required types not found therefore the kind of pattern considered * will not be found. */ if (googEventsEventHandlerType == null || googDisposableInterfaceType == null) { return; } // Seed list of disposable stype eventfulTypes = new HashSet<>(); eventfulTypes.add(googEventsEventHandlerType); // Construct eventizer graph if (checkingPolicy == DisposalCheckingPolicy.AGGRESSIVE) { NodeTraversal.traverseTyped(compiler, root, new ComputeEventizeTraversal()); computeEventful(); } /* * eventfulObjectMap maps a eventful object's "name" to its corresponding * EventfulObjectState which tracks the state (allocated, disposed of) * as well as allocation site. */ eventfulObjectMap = new HashMap<>(); // Traverse tree NodeTraversal.traverseTyped(compiler, root, new Traversal()); /* * Scan eventfulObjectMap for allocated eventful objects that * had no dispose calls. */ for (EventfulObjectState e : eventfulObjectMap.values()) { Node n = e.allocationSite; if (e.seen == SeenType.ALLOCATED) { compiler.report(JSError.make(n, EVENTFUL_OBJECT_NOT_DISPOSED)); } else if (e.seen == SeenType.ALLOCATED_LOCALLY && checkingPolicy == DisposalCheckingPolicy.AGGRESSIVE) { compiler.report(JSError.make(n, EVENTFUL_OBJECT_PURELY_LOCAL)); } } } private void computeEventful() { /* * Topological order of Eventize DAG */ String[] order = new String[eventizes.keySet().size()]; /* * Perform topological sort */ int white = 0; int gray = 1; int black = 2; int last = eventizes.keySet().size() - 1; Map color = new HashMap<>(); Stack dfsStack = new Stack<>(); /* * Initialize color. * Some types are only on one or the other side of the * inference. */ for (Map.Entry> eventizesEntry : Multimaps.asMap(eventizes).entrySet()) { color.put(eventizesEntry.getKey(), white); for (String s : eventizesEntry.getValue()) { color.put(s, white); } } int indx = 0; for (String s : eventizes.keySet()) { dfsStack.push(s); while (!dfsStack.isEmpty()) { String top = dfsStack.pop(); if (!color.containsKey(top)) { continue; } if (color.get(top) == white) { color.put(top, gray); dfsStack.push(top); // for v in Adj[s] if (eventizes.containsKey(top)) { for (String v : eventizes.get(top)) { if (color.get(v) == white) { dfsStack.push(v); } } } } else if (color.get(top) == gray && eventizes.containsKey(top)) { order[last - indx] = top; ++indx; color.put(top, black); } } } /* * Propagate eventfulness by iterating in topological order */ for (String s : order) { if (eventfulTypes.contains(typeRegistry.getType(s))) { for (String v : eventizes.get(s)) { eventfulTypes.add((JSType) typeRegistry.getType(v)); } } } } private JSType maybeReturnDisposedType(Node n, boolean checkDispose) { /* * Checks for: * - Y.registerDisposable(X) * (Y has to be of type goog.Disposable) * - X.dispose() * - goog.dispose(X) * - X.removeAll() (X is of type goog.events.EventHandler) * - .property(X) or Y.push(X) */ Node first = n.getFirstChild(); if (first == null || !first.isQualifiedName()) { return null; } String property = first.getQualifiedName(); if (property.endsWith(".registerDisposable")) { /* * Ensure object is of type disposable */ Node base = first.getFirstChild(); JSType baseType = base.getJSType(); if (baseType == null || !isPossiblySubtype(baseType, googDisposableInterfaceType)) { return null; } return n.getLastChild().getJSType(); } if (checkDispose) { if (property.equals("goog.dispose")) { return n.getLastChild().getJSType(); } if (property.endsWith(".dispose")) { /* * n -> call * n.firstChild -> "dispose" * n.firstChild.firstChild -> object */ return n.getFirstFirstChild().getJSType(); } } return null; } /* * Compute eventize relationship graph. */ private class ComputeEventizeTraversal extends AbstractPostOrderCallback implements ScopedCallback { /* * Keep track of whether in the constructor or disposal scope. */ Stack isConstructorStack; Stack isDisposalStack; public ComputeEventizeTraversal() { isConstructorStack = new Stack<>(); isDisposalStack = new Stack<>(); eventizes = HashMultimap.create(); } private Boolean inConstructorScope() { Preconditions.checkNotNull(isConstructorStack); if (!isDisposalStack.isEmpty()) { return isConstructorStack.peek(); } return null; } private Boolean inDisposalScope() { Preconditions.checkNotNull(isDisposalStack); if (!isDisposalStack.isEmpty()) { return isDisposalStack.peek(); } return null; } /* * Filter types not interested in for eventize graph */ private boolean collectorFilterType(JSType type) { if (type == null) { return true; } return type.isEmptyType() || type.isUnknownType() || !isPossiblySubtype(type, googDisposableInterfaceType); } /* * Log that thisType eventizes thatType. */ private void addEventize(JSType thisType, JSType thatType) { if (collectorFilterType(thisType) || collectorFilterType(thatType) || thisType.isEquivalentTo(thatType)) { return; } String className = thisType.getDisplayName(); if (thatType.isUnionType()) { UnionType ut = thatType.toMaybeUnionType(); for (JSType type : ut.getAlternates()) { if (type.isObject()) { addEventizeClass(className, type); } } } else { addEventizeClass(className, thatType); } } private void addEventizeClass(String className, JSType thatType) { String propertyJsTypeName = thatType.getDisplayName(); eventizes.put(propertyJsTypeName, className); } @Override public void enterScope(NodeTraversal t) { Node n = t.getScopeRoot(); boolean isConstructor = false; boolean isInDisposal = false; String functionName = null; /* * TypedScope entered is a function definition */ if (n.isFunction()) { functionName = NodeUtil.getName(n); /* * Skip anonymous functions */ if (functionName != null) { JSDocInfo jsDocInfo = NodeUtil.getBestJSDocInfo(n); if (jsDocInfo != null) { /* * Record constructor of a type */ if (jsDocInfo.isConstructor()) { isConstructor = true; /* * Initialize eventizes relationship */ if (t.getTypedScope() != null && t.getTypedScope().getTypeOfThis() != null) { ObjectType objectType = ObjectType.cast(t.getTypedScope() .getTypeOfThis().dereference()); /* * Eventize due to inheritance */ while (objectType != null) { objectType = objectType.getImplicitPrototype(); if (objectType == null) { break; } if (objectType.getDisplayName().endsWith("prototype")) { continue; } addEventize((JSType) compiler.getTypeIRegistry().getType(functionName), objectType); /* * Don't add transitive eventize edges here, it will be * taken care of in computeEventful */ break; } } } } /* * Indicate within a disposeInternal member */ if (functionName.endsWith(".disposeInternal")) { isInDisposal = true; } } isConstructorStack.push(isConstructor); isDisposalStack.push(isInDisposal); } else { isConstructorStack.push(inConstructorScope()); isDisposalStack.push(inDisposalScope()); } } @Override public void exitScope(NodeTraversal t) { isConstructorStack.pop(); isDisposalStack.pop(); } /* * Is the current node a call to goog.events.unlisten */ private void isGoogEventsUnlisten(Node n) { Preconditions.checkArgument(n.getChildCount() > 3); Node listener = n.getChildAtIndex(3); Node objectWithListener = n.getSecondChild(); if (!objectWithListener.isQualifiedName()) { return; } if (listener.isFunction()) { /* * Anonymous function */ compiler.report(JSError.make(n, UNLISTEN_WITH_ANONBOUND)); } else if (listener.isCall()) { if (!listener.getFirstChild().isQualifiedName()) { /* * Anonymous function */ compiler.report(JSError.make(n, UNLISTEN_WITH_ANONBOUND)); } else if (listener.getFirstChild().matchesQualifiedName("goog.bind")) { /* * Using goog.bind to unlisten */ compiler.report(JSError.make(n, UNLISTEN_WITH_ANONBOUND)); } } } private void visitCall(NodeTraversal t, Node n) { Node functionCalled = n.getFirstChild(); if (functionCalled == null || !functionCalled.isQualifiedName()) { return; } JSType typeOfThis = getTypeOfThisForScope(t); if (typeOfThis == null) { return; } /* * Class considered eventful if there is an unlisten call in the * disposal. */ if (functionCalled.matchesQualifiedName("goog.events.unlisten")) { if (inDisposalScope()) { eventfulTypes.add(typeOfThis); } isGoogEventsUnlisten(n); } if (inDisposalScope() && functionCalled.matchesQualifiedName("goog.events.removeAll")) { eventfulTypes.add(typeOfThis); } /* * If member with qualified name gets disposed of when this class * gets disposed, consider the member type as an eventizer of this * class. */ JSType disposedType = maybeReturnDisposedType(n, inDisposalScope()); if (!collectorFilterType(disposedType)) { addEventize(getTypeOfThisForScope(t), disposedType); } } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case CALL: visitCall(t, n); break; default: break; } } } private class Traversal extends AbstractPostOrderCallback implements ScopedCallback { /* * Checks if the input node correspond to the creation of an eventful object */ private boolean createsEventfulObject(Node n) { Node first = n.getFirstChild(); JSType type = n.getJSType(); if (first == null || !first.isQualifiedName() || type.isEmptyType() || type.isUnknownType()) { return false; } boolean isOfTypeNeedingDisposal = false; for (JSType disposableType : eventfulTypes) { if (type.isSubtype(disposableType)) { isOfTypeNeedingDisposal = true; break; } } return isOfTypeNeedingDisposal; } /* * This function traverses the current scope to see if a locally * defined eventful object is assigned to a live-out variable. * * Note: This function could be called multiple times to traverse * the same scope if multiple local eventful objects are created in the * scope. */ private Node localEventfulObjectAssign( NodeTraversal t, Node propertyNode) { Node parent; if (!t.getTypedScope().isGlobal()) { /* * In function */ parent = NodeUtil.getFunctionBody(t.getScopeRoot()); } else { /* * In global scope */ parent = t.getScopeRoot().getFirstChild(); } /* * Check to see if locally created EventHandler is assigned to field */ for (Node sibling : parent.children()) { if (sibling.isExprResult()) { Node assign = sibling.getFirstChild(); if (assign.isAssign()) { // assign.getLastChild().isEquivalentTo(propertyNode) did not work if (propertyNode.matchesQualifiedName(assign.getLastChild())) { if (!assign.getFirstChild().isName()) { return assign.getFirstChild(); } } } } } /* * Eventful object created and assigned to a local variable which is not * assigned to another variable in a way to allow disposal. */ String key = generateKey(t, propertyNode, false); if (key == null) { return null; } EventfulObjectState e; if (eventfulObjectMap.containsKey(key)) { e = eventfulObjectMap.get(key); if (e.seen == SeenType.ALLOCATED) { e.seen = SeenType.ALLOCATED_LOCALLY; } } else { e = new EventfulObjectState(); e.seen = SeenType.ALLOCATED_LOCALLY; eventfulObjectMap.put(key, e); } e.allocationSite = propertyNode; return null; } /* * Record the creation of a new eventful object. */ private void visitNew(NodeTraversal t, Node n, Node parent) { if (!createsEventfulObject(n)) { return; } /* * Insert allocation site and construct into eventfulObjectMap */ String key; Node propertyNode; /* * Handles (E is an eventful class): * - object.something = new E(); * - local = new E(); * - var local = new E(); */ if (parent.isAssign()) { propertyNode = parent.getFirstChild(); } else { propertyNode = parent; } key = generateKey(t, propertyNode, false); if (key == null) { return; } EventfulObjectState e; if (eventfulObjectMap.containsKey(key)) { e = eventfulObjectMap.get(key); } else { e = new EventfulObjectState(); e.seen = SeenType.ALLOCATED; eventfulObjectMap.put(key, e); } e.allocationSite = propertyNode; /* * Check if locally defined eventful object is assigned to global variable * and create an entry mapping to the previous site. */ if (propertyNode.isName()) { Node globalVarNode = localEventfulObjectAssign(t, propertyNode); if (globalVarNode != null) { key = generateKey(t, globalVarNode, false); if (key == null) { /* * Local variable is assigned to an array or in a manner requiring * a function call. */ e.seen = SeenType.POSSIBLY_DISPOSED; return; } eventfulObjectMap.put(key, e); } } } private void addDisposeArgumentsMatched(Map> map, Node n, String property, List foundDisposeCalls) { for (Map.Entry> disposeCallsEntry : map.entrySet()) { if (property.endsWith(disposeCallsEntry.getKey())) { List disposeArguments = disposeCallsEntry.getValue(); // Dispose specific arguments only Node t = n.getNext(); int tsArgument = 0; for (Integer disposeArgument : disposeArguments) { switch (disposeArgument) { // Dispose all arguments case DISPOSE_ALL: for (Node tt = n.getNext(); tt != null; tt = tt.getNext()) { foundDisposeCalls.add(tt); } break; // Dispose objects called on case DISPOSE_SELF: Node calledOn = n.getFirstChild(); foundDisposeCalls.add(calledOn); break; default: // The current item pointed to by t is beyond that requested in // current array element. if (tsArgument > disposeArgument) { t = n.getNext(); tsArgument = 0; } for (; tsArgument < disposeArgument && t != null; ++tsArgument) { t = t.getNext(); } if (tsArgument == disposeArgument && t != null) { foundDisposeCalls.add(t); } break; } } } } } private List maybeGetValueNodesFromCall(Node n) { List ret = new ArrayList<>(); Node first = n.getFirstChild(); if (first == null || !first.isQualifiedName()) { return ret; } String property = first.getQualifiedName(); Node base = first.getFirstChild(); JSType baseType = null; if (base != null) { baseType = base.getJSType(); } for (Map.Entry>> disposeCallEntry : disposeCalls.entrySet()) { JSType key = disposeCallEntry.getKey(); if (key == null || (baseType != null && isPossiblySubtype(baseType, key))) { addDisposeArgumentsMatched(disposeCallEntry.getValue(), first, property, ret); } } return ret; } /* * Look for calls to an eventful object's disposal functions. * (dispose or removeAll will remove all event listeners from * an EventHandler). */ private void visitCall(NodeTraversal t, Node n) { // Filter the calls to find a "dispose" call List variableNodes = maybeGetValueNodesFromCall(n); for (Node variableNode : variableNodes) { Preconditions.checkState(variableNode != null); // Only consider removals on eventful object boolean isTrackedRemoval = false; JSType vnType = variableNode.getJSType(); for (JSType type : eventfulTypes) { if (isPossiblySubtype(vnType, type)) { isTrackedRemoval = true; } } if (!isTrackedRemoval) { continue; } String key = generateKey(t, variableNode, false); if (key == null) { continue; } eventfulObjectDisposed(t, variableNode); } } /** * Dereference a type, autoboxing it and filtering out null. * From {@link CheckAccessControls} */ private JSType dereference(JSType type) { return type == null ? null : type.dereference(); } /* * Check function definitions to add custom dispose methods. */ public void visitFunction(NodeTraversal t, Node n) { Preconditions.checkArgument(n.isFunction()); JSDocInfo jsDocInfo = NodeUtil.getBestJSDocInfo(n); // Function annotated to dispose of if (jsDocInfo != null && jsDocInfo.isDisposes()) { JSType type = n.getJSType(); if (type == null || type.isUnknownType()) { return; } FunctionType funType = type.toMaybeFunctionType(); Node paramNode = NodeUtil.getFunctionParameters(n).getFirstChild(); List positionalDisposedParameters = new ArrayList<>(); if (jsDocInfo.disposesOf("*")) { positionalDisposedParameters.add(DISPOSE_ALL); } else { // Record index of parameters that are disposed. for (int index = 0; index < funType.getMaxArguments(); ++index) { // Bail out if the paramNode is not there. if (paramNode == null) { break; } if (jsDocInfo.disposesOf(paramNode.getString())) { positionalDisposedParameters.add(index); } paramNode = paramNode.getNext(); } } addDisposeCall(NodeUtil.getName(n), positionalDisposedParameters); } } /* * Track assignments to see if a private field is being * overwritten. * * Assigning to an array element is taken care of by the generateKey * returning null on array ("complex") variable names. */ public void visitAssign(NodeTraversal t, Node n) { Node assignedTo = n.getFirstChild(); JSType assignedToType = assignedTo.getJSType(); if (assignedToType == null || assignedToType.isEmptyType()) { return; } if (n.getFirstChild().isGetProp()) { boolean isTrackedAssign = false; for (JSType disposalType : eventfulTypes) { if (assignedToType.isSubtype(disposalType)) { isTrackedAssign = true; break; } } if (!isTrackedAssign) { return; } JSDocInfo di = n.getJSDocInfo(); ObjectType objectType = ObjectType.cast(dereference(n.getFirstFirstChild() .getJSType())); String propertyName = n.getFirstChild().getLastChild().getString(); boolean fieldIsPrivate = (di != null) && (di.getVisibility() == Visibility.PRIVATE); /* * See if field is defined as private in superclass */ while (objectType != null) { di = null; objectType = objectType.getImplicitPrototype(); if (objectType == null) { break; } /* * Skip prototype definitions: * Don't flag a field declared private in assignment as well * as in prototype declaration * Assumption: The inheritance hierarchy is similar to * class * class.prototype * superclass * superclass.prototype */ if (objectType.getDisplayName().endsWith("prototype")) { continue; } di = objectType.getOwnPropertyJSDocInfo(propertyName); if (di != null) { if (fieldIsPrivate || di.getVisibility() == Visibility.PRIVATE) { compiler.report( t.makeError(n, OVERWRITE_PRIVATE_EVENTFUL_OBJECT)); break; } } } } } /* * Filter out any eventful objects returned. */ private void visitReturn(NodeTraversal t, Node n) { Node variableNode = n.getFirstChild(); if (variableNode == null) { return; } if (!variableNode.isArrayLit()) { eventfulObjectDisposed(t, variableNode); } else { for (Node child : variableNode.children()) { eventfulObjectDisposed(t, child); } } } /* * Mark an eventful object as being disposed. */ private void eventfulObjectDisposed(NodeTraversal t, Node variableNode) { String key = generateKey(t, variableNode, false); if (key == null) { return; } EventfulObjectState e = eventfulObjectMap.get(key); if (e == null) { e = new EventfulObjectState(); eventfulObjectMap.put(key, e); } e.seen = SeenType.POSSIBLY_DISPOSED; } @Override public void enterScope(NodeTraversal t) { TypedScope scope = t.getTypedScope(); if (scope.getVarCount() > LiveVariablesAnalysis.MAX_VARIABLES_TO_ANALYZE) { // Too many variables to analyze, so just assume that all eventful objects are // disposed. This will miss some errors but only in very large scopes. for (TypedVar v : scope.getVarIterable()) { eventfulObjectDisposed(t, v.getNode()); } return; } /* * Local variables captured in scope are filtered at present. * LiveVariableAnalysis used to filter such variables. */ ControlFlowGraph cfg = t.getControlFlowGraph(); LiveVariablesAnalysis liveness = new LiveVariablesAnalysis(cfg, t.getTypedScope(), compiler); liveness.analyze(); for (TypedVar v : ((Set) liveness.getEscapedLocals())) { eventfulObjectDisposed(t, v.getNode()); } } @Override public void exitScope(NodeTraversal t) { } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case ASSIGN: visitAssign(t, n); break; case CALL: visitCall(t, n); break; case FUNCTION: visitFunction(t, n); break; case NEW: visitNew(t, n, parent); break; case RETURN: visitReturn(t, n); break; default: break; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy