/** * If we haven't found a return value yet, try to look at the "return" statements in the function. */ FunctionTypeBuilder inferReturnStatementsAsLastResort(@Nullable Node functionBlock) { if (functionBlock == null || compiler.getInput(sourceName).isExtern()) { return this; } Preconditions.checkArgument(functionBlock.getType() == Token.BLOCK); if (returnType == null) { boolean hasNonEmptyReturns = false; List<Node> worklist = Lists.newArrayList(functionBlock); while (!worklist.isEmpty()) { Node current = worklist.remove(worklist.size() - 1); int cType = current.getType(); if (cType == Token.RETURN && current.getFirstChild() != null || cType == Token.THROW) { hasNonEmptyReturns = true; break; } else if (NodeUtil.isStatementBlock(current) || NodeUtil.isControlStructure(current)) { for (Node child = current.getFirstChild(); child != null; child = child.getNext()) { worklist.add(child); } } } if (!hasNonEmptyReturns) { returnType = typeRegistry.getNativeType(VOID_TYPE); returnTypeInferred = true; } } return this; }
public void testLinenoCharnoObjectLiteral() throws Exception { Node n = parse("\n\n var a = {a:0\n,b :1};").getFirstChild().getFirstChild().getFirstChild(); assertEquals(Token.OBJECTLIT, n.getType()); assertEquals(3, n.getLineno()); assertEquals(9, n.getCharno()); Node key = n.getFirstChild(); assertEquals(Token.STRING_KEY, key.getType()); assertEquals(3, key.getLineno()); assertEquals(10, key.getCharno()); Node value = key.getFirstChild(); assertEquals(Token.NUMBER, value.getType()); assertEquals(3, value.getLineno()); assertEquals(12, value.getCharno()); key = key.getNext(); assertEquals(Token.STRING_KEY, key.getType()); assertEquals(4, key.getLineno()); assertEquals(1, key.getCharno()); value = key.getFirstChild(); assertEquals(Token.NUMBER, value.getType()); assertEquals(4, value.getLineno()); assertEquals(4, value.getCharno()); }
/** * Gets whether a node is a CALL node whose return value should be stripped. A call's return * value should be stripped if the function getting called is a static method in a class that * gets stripped. For example, if "goog.debug.Logger" is a strip name, then this function * returns true for a call such as "goog.debug.Logger.getLogger(...)". It may also simply be a * function that is getting stripped. For example, if "getLogger" is a strip name, but not * "goog.debug.Logger", this will still return true. * * @param n A node (typically a CALL node) * @return Whether the call's return value should be stripped */ boolean isCallWhoseReturnValueShouldBeStripped(@Nullable Node n) { return n != null && (n.getType() == Token.CALL || n.getType() == Token.NEW) && n.hasChildren() && (qualifiedNameBeginsWithStripType(n.getFirstChild()) || nameEndsWithFieldNameToStrip(n.getFirstChild())); }
public void testJSDocAttachment18() { Node fn = parse("function f() { " + " var x = /** @type {string} */ (y);" + "};").getFirstChild(); assertEquals(Token.FUNCTION, fn.getType()); Node cast = fn.getLastChild().getFirstChild().getFirstChild().getFirstChild(); assertEquals(Token.CAST, cast.getType()); }
/** If this is an assign to a variable or its property, return it. Otherwise, return null. */ static Assign maybeCreateAssign(Node assignNode) { Preconditions.checkState(NodeUtil.isAssignmentOp(assignNode)); // Skip one level of GETPROPs or GETELEMs. // // Don't skip more than one level, because then we get into // situations where assigns to properties of properties will always // trigger side-effects, and the variable they're on cannot be removed. boolean isPropAssign = false; Node current = assignNode.getFirstChild(); if (NodeUtil.isGet(current)) { current = current.getFirstChild(); isPropAssign = true; if (current.getType() == Token.GETPROP && current.getLastChild().getString().equals("prototype")) { // Prototype properties sets should be considered like normal // property sets. current = current.getFirstChild(); } } if (current.getType() == Token.NAME) { return new Assign(assignNode, current, isPropAssign); } return null; }
/** Replace the current assign with its right hand side. */ void remove() { Node parent = assignNode.getParent(); if (mayHaveSecondarySideEffects) { Node replacement = assignNode.getLastChild().detachFromParent(); // Aggregate any expressions in GETELEMs. for (Node current = assignNode.getFirstChild(); current.getType() != Token.NAME; current = current.getFirstChild()) { if (current.getType() == Token.GETELEM) { replacement = new Node(Token.COMMA, current.getLastChild().detachFromParent(), replacement); replacement.copyInformationFrom(current); } } parent.replaceChild(assignNode, replacement); } else { Node gramps = parent.getParent(); if (parent.getType() == Token.EXPR_RESULT) { gramps.removeChild(parent); } else { parent.replaceChild(assignNode, assignNode.getLastChild().detachFromParent()); } } }
private void scanRoot(Node n, Scope parent) { if (n.getType() == Token.FUNCTION) { sourceName = (String) n.getProp(Node.SOURCENAME_PROP); final Node fnNameNode = n.getFirstChild(); final Node args = fnNameNode.getNext(); final Node body = args.getNext(); // Bleed the function name into the scope, if it hasn't // been declared in the outer scope. String fnName = fnNameNode.getString(); if (!fnName.isEmpty() && NodeUtil.isFunctionExpression(n)) { declareVar(fnName, fnNameNode, n, null, null, n); } // Args: Declare function variables Preconditions.checkState(args.getType() == Token.LP); for (Node a = args.getFirstChild(); a != null; a = a.getNext()) { Preconditions.checkState(a.getType() == Token.NAME); declareVar(a.getString(), a, args, n, null, n); } // Body scanVars(body, n); } else { // It's the global block Preconditions.checkState(scope.getParent() == null); scanVars(n, null); } }
public void onRedeclaration( Scope s, String name, Node n, Node parent, Node gramps, Node nodeWithLineNumber) { // Don't allow multiple variables to be declared at the top level scope if (scope.isGlobal()) { Scope.Var origVar = scope.getVar(name); Node origParent = origVar.getParentNode(); if (origParent.getType() == Token.CATCH && parent.getType() == Token.CATCH) { // Okay, both are 'catch(x)' variables. return; } boolean allowDupe = false; JSDocInfo info = n.getJSDocInfo(); if (info == null) { info = parent.getJSDocInfo(); } allowDupe = info != null && info.getSuppressions().contains("duplicate"); if (!allowDupe) { compiler.report( JSError.make( sourceName, nodeWithLineNumber, VAR_MULTIPLY_DECLARED_ERROR, name, (origVar.input != null ? origVar.input.getName() : "??"))); } } else if (name.equals(ARGUMENTS) && !NodeUtil.isVarDeclaration(n)) { // Disallow shadowing "arguments" as we can't handle with our current // scope modeling. compiler.report(JSError.make(sourceName, nodeWithLineNumber, VAR_ARGUMENTS_SHADOWED_ERROR)); } }
@Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.getType() == Token.GETPROP) { handleGetProp(t, n); } else if (n.getType() == Token.OBJECTLIT) { handleObjectLit(t, n); } }
/** * Gets whether a name ends with a field name that should be stripped. For example, this * function would return true when passed "this.logger" or "a.b.c.myLogger" if "logger" is a * strip name. * * @param n A node (typically a GETPROP node) * @return Whether the name ends with a field name that should be stripped */ boolean nameEndsWithFieldNameToStrip(@Nullable Node n) { if (n != null && n.getType() == Token.GETPROP) { Node propNode = n.getLastChild(); return propNode != null && propNode.getType() == Token.STRING && isStripName(propNode.getString()); } return false; }
public void testObjectLiteralDoc1() { Node n = parse("var x = {/** @type {number} */ 1: 2};"); Node objectLit = n.getFirstChild().getFirstChild().getFirstChild(); assertEquals(Token.OBJECTLIT, objectLit.getType()); Node number = objectLit.getFirstChild(); assertEquals(Token.STRING_KEY, number.getType()); assertNotNull(number.getJSDocInfo()); }
/** Scans and gather variables declarations under a Node */ private void scanVars(Node n, Node parent) { switch (n.getType()) { case Token.VAR: // Declare all variables. e.g. var x = 1, y, z; for (Node child = n.getFirstChild(); child != null; ) { Node next = child.getNext(); Preconditions.checkState(child.getType() == Token.NAME); String name = child.getString(); declareVar(name, child, n, parent, null, n); child = next; } return; case Token.FUNCTION: if (NodeUtil.isFunctionExpression(n)) { return; } String fnName = n.getFirstChild().getString(); if (fnName.isEmpty()) { // This is invalid, but allow it so the checks can catch it. return; } declareVar(fnName, n.getFirstChild(), n, parent, null, n); return; // should not examine function's children case Token.CATCH: Preconditions.checkState(n.getChildCount() == 2); Preconditions.checkState(n.getFirstChild().getType() == Token.NAME); // the first child is the catch var and the third child // is the code block final Node var = n.getFirstChild(); final Node block = var.getNext(); declareVar(var.getString(), var, n, parent, null, n); scanVars(block, n); return; // only one child to scan case Token.SCRIPT: sourceName = (String) n.getProp(Node.SOURCENAME_PROP); break; } // Variables can only occur in statement-level nodes, so // we only need to traverse children in a couple special cases. if (NodeUtil.isControlStructure(n) || NodeUtil.isStatementBlock(n)) { for (Node child = n.getFirstChild(); child != null; ) { Node next = child.getNext(); scanVars(child, n); child = next; } } }
/** * Locates the array literal specified by the <code>array</code> node, in the source using the * source location information provided by the node, and inserts <code>str</code> as a string * literal (quoted) in the source at the end of the array. * * @param array Node with source position information * @param str the string to insert into the source at the end of the specified array */ public void appendToArrayLit(Node array, String str) { final String sourceMethod = "appendToArrayLit"; // $NON-NLS-1$ if (isTraceLogging) { log.entering(JSSource.class.getName(), sourceMethod, new Object[] {array, str}); } // Get the node for the last element in the array Node lastChild = array.getLastChild(); // If token is not a string, then punt if (lastChild.getType() != Token.STRING && lastChild.getType() != Token.NAME) { if (log.isLoggable(Level.WARNING)) { log.warning( "Last element of array at " + mid + "(" + array.getLineno() + "," + array.getCharno() + ") is type " + array.getType()); // $NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } if (isTraceLogging) { log.exiting(JSSource.class.getName(), sourceMethod); } return; } int len = lastChild.getString().length() + (lastChild.getType() == Token.STRING ? 2 : 0); // add 2 for string quotes // Search the source for the closing array bracket ']', skipping over // whitespace and comments. In order to be valid javascript, the next // token must be the closing bracket. int lineno = lastChild.getLineno(); int charno = lastChild.getCharno() + len; PositionLocator pos = new PositionLocator(lineno, charno); if (pos.findNextJSToken() == ']') { insert(",\"" + str + "\"", pos.getLineno(), pos.getCharno()); // $NON-NLS-1$ //$NON-NLS-2$ } else { if (log.isLoggable(Level.WARNING)) { log.warning( "Closing array bracket not found in " + mid + "(" + lineno + "," + charno + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } } if (isTraceLogging) { log.exiting(JSSource.class.getName(), sourceMethod); } }
public void testJSDocAttachment10() { Node varNode = parse("/** x\n */var a;").getFirstChild(); // VAR assertEquals(Token.VAR, varNode.getType()); // NAME Node nameNode = varNode.getFirstChild(); assertEquals(Token.NAME, nameNode.getType()); assertNull(nameNode.getJSDocInfo()); }
public void testLinenoCharnoGetProp2() throws Exception { Node getprop = parse("\n foo.\nbar").getFirstChild().getFirstChild(); assertEquals(Token.GETPROP, getprop.getType()); assertEquals(2, getprop.getLineno()); assertEquals(1, getprop.getCharno()); Node name = getprop.getFirstChild().getNext(); assertEquals(Token.STRING, name.getType()); assertEquals(3, name.getLineno()); assertEquals(0, name.getCharno()); }
private void validateSwitchMember(Node n) { switch (n.getType()) { case Token.CASE: validateCase(n); return; case Token.DEFAULT_CASE: validateDefaultCase(n); return; default: violation("Expected switch member but was " + Token.name(n.getType()), n); } }
public void visit(NodeTraversal t, Node n, Node parent) { if (n.getType() != Token.FUNCTION) { return; } int id = functionNames.getFunctionId(n); if (id < 0) { // Function node was added during compilation; don't instrument. return; } if (reportFunctionName.length() != 0) { Node body = n.getFirstChild().getNext().getNext(); Node call = new Node( Token.CALL, Node.newString(Token.NAME, reportFunctionName), Node.newNumber(id)); Node expr = new Node(Token.EXPR_RESULT, call); body.addChildToFront(expr); compiler.reportCodeChange(); } if (reportFunctionExitName.length() != 0) { Node body = n.getFirstChild().getNext().getNext(); (new InstrumentReturns(id)).process(body); } if (definedFunctionName.length() != 0) { Node call = new Node( Token.CALL, Node.newString(Token.NAME, definedFunctionName), Node.newNumber(id)); Node expr = NodeUtil.newExpr(call); Node addingRoot = null; if (NodeUtil.isFunctionDeclaration(n)) { JSModule module = t.getModule(); addingRoot = compiler.getNodeForCodeInsertion(module); addingRoot.addChildToFront(expr); } else { Node beforeChild = n; for (Node ancestor : n.getAncestors()) { int type = ancestor.getType(); if (type == Token.BLOCK || type == Token.SCRIPT) { addingRoot = ancestor; break; } beforeChild = ancestor; } addingRoot.addChildBefore(expr, beforeChild); } compiler.reportCodeChange(); } }
// Looks at the type AST without evaluating it private boolean isUnionWithUndefined(Node n) { if (n == null || n.getType() != Token.PIPE) { return false; } for (Node child : n.children()) { if (child.getType() == Token.VOID || child.getType() == Token.STRING && (child.getString().equals("void") || child.getString().equals("undefined"))) { return true; } } return false; }
/** * Traverses a function, which creates a new scope in javascript. * * <p>Note that CATCH blocks also create a new scope, but only for the catch variable. * Declarations within the block actually belong to the enclosing scope. Because we don't remove * catch variables, there's no need to treat CATCH blocks differently like we do functions. */ private void traverseFunction(Node n, Scope parentScope) { Preconditions.checkState(n.getChildCount() == 3); Preconditions.checkState(n.getType() == Token.FUNCTION); final Node body = n.getLastChild(); Preconditions.checkState(body.getNext() == null && body.getType() == Token.BLOCK); Scope fnScope = new SyntacticScopeCreator(compiler).createScope(n, parentScope); traverseNode(body, n, fnScope); collectMaybeUnreferencedVars(fnScope); allFunctionScopes.add(fnScope); }
private void fillInFunTypeBuilder( Node jsdocNode, RawNominalType ownerType, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters, FunctionTypeBuilder builder) throws UnknownTypeException { Node child = jsdocNode.getFirstChild(); if (child.getType() == Token.THIS) { if (ownerType == null) { builder.addReceiverType(getThisOrNewType(child.getFirstChild(), registry, typeParameters)); } child = child.getNext(); } else if (child.getType() == Token.NEW) { Node newTypeNode = child.getFirstChild(); JSType t = getThisOrNewType(newTypeNode, registry, typeParameters); if (!t.isSubtypeOf(JSType.TOP_OBJECT) && (!t.hasTypeVariable() || t.hasScalar())) { warnings.add(JSError.make(newTypeNode, NEW_EXPECTS_OBJECT_OR_TYPEVAR, t.toString())); } builder.addNominalType(t); child = child.getNext(); } if (child.getType() == Token.PARAM_LIST) { for (Node arg = child.getFirstChild(); arg != null; arg = arg.getNext()) { try { switch (arg.getType()) { case Token.EQUALS: builder.addOptFormal( getTypeFromCommentHelper(arg.getFirstChild(), registry, typeParameters)); break; case Token.ELLIPSIS: Node restNode = arg.getFirstChild(); builder.addRestFormals( restNode == null ? JSType.UNKNOWN : getTypeFromCommentHelper(restNode, registry, typeParameters)); break; default: builder.addReqFormal(getTypeFromCommentHelper(arg, registry, typeParameters)); break; } } catch (FunctionTypeBuilder.WrongParameterOrderException e) { warnings.add(JSError.make(jsdocNode, WRONG_PARAMETER_ORDER)); builder.addPlaceholderFormal(); } } child = child.getNext(); } builder.addRetType(getTypeFromCommentHelper(child, registry, typeParameters)); }
public void testJSDocAttachment1() { Node varNode = parse("/** @type number */var a;").getFirstChild(); // VAR assertEquals(Token.VAR, varNode.getType()); JSDocInfo info = varNode.getJSDocInfo(); assertNotNull(info); assertTypeEquals(NUMBER_TYPE, info.getType()); // NAME Node nameNode = varNode.getFirstChild(); assertEquals(Token.NAME, nameNode.getType()); assertNull(nameNode.getJSDocInfo()); }
/** * Gets all the control flow edges from some node with the first token to * some node with the second token. */ private static List<DiGraphEdge<Node, Branch>> getAllEdges( ControlFlowGraph<Node> cfg, int startToken, int endToken) { List<DiGraphEdge<Node, Branch>> edges = getAllEdges(cfg); Iterator<DiGraphEdge<Node, Branch>> it = edges.iterator(); while (it.hasNext()) { DiGraphEdge<Node, Branch> edge = it.next(); Node startNode = edge.getSource().getValue(); Node endNode = edge.getDestination().getValue(); if (startNode == null || endNode == null || startNode.getType() != startToken || endNode.getType() != endToken) { it.remove(); } } return edges; }
/** * Connects cfgNode to the proper CATCH block if target subtree might throw an exception. If there * are FINALLY blocks reached before a CATCH, it will make the corresponding entry in finallyMap. */ private void connectToPossibleExceptionHandler(Node cfgNode, Node target) { if (mayThrowException(target) && !exceptionHandler.isEmpty()) { Node lastJump = cfgNode; for (Node handler : exceptionHandler) { if (NodeUtil.isFunction(handler)) { return; } Preconditions.checkState(handler.getType() == Token.TRY); Node catchBlock = NodeUtil.getCatchBlock(handler); if (!NodeUtil.hasCatchHandler(catchBlock)) { // No catch but a FINALLY. if (lastJump == cfgNode) { createEdge(cfgNode, Branch.ON_EX, handler.getLastChild()); } else { finallyMap.put(lastJump, handler.getLastChild()); } } else { // Has a catch. if (lastJump == cfgNode) { createEdge(cfgNode, Branch.ON_EX, catchBlock); return; } else { finallyMap.put(lastJump, catchBlock); } } lastJump = handler; } } }
private void handleContinue(Node node) { String label = null; if (node.hasChildren()) { label = node.getFirstChild().getString(); } Node cur; Node lastJump; // Similar to handBreak's logic with a few minor variation. Node parent = node.getParent(); for (cur = node, lastJump = node; !isContinueTarget(cur, parent, label); cur = parent, parent = parent.getParent()) { if (cur.getType() == Token.TRY && NodeUtil.hasFinally(cur)) { if (lastJump == node) { createEdge(lastJump, Branch.UNCOND, cur.getLastChild()); } else { finallyMap.put(lastJump, computeFallThrough(cur.getLastChild())); } lastJump = cur; } Preconditions.checkState(parent != null, "Cannot find continue target."); } Node iter = cur; if (cur.getChildCount() == 4) { iter = cur.getFirstChild().getNext().getNext(); } if (lastJump == node) { createEdge(node, Branch.UNCOND, iter); } else { finallyMap.put(lastJump, iter); } }
/** @return true if this node marks the start of a new basic block */ private static boolean isBlockBoundary(Node n, Node parent) { if (parent != null) { switch (parent.getType()) { case Token.DO: case Token.FOR: case Token.FOR_OF: case Token.TRY: case Token.WHILE: case Token.WITH: // NOTE: TRY has up to 3 child blocks: // TRY // BLOCK // BLOCK // CATCH // BLOCK // Note that there is an explicit CATCH token but no explicit // FINALLY token. For simplicity, we consider each BLOCK // a separate basic BLOCK. return true; case Token.AND: case Token.HOOK: case Token.IF: case Token.OR: // The first child of a conditional is not a boundary, // but all the rest of the children are. return n != parent.getFirstChild(); } } return n.isCase(); }
/** Creates an instance for a function that might be a constructor. */ FunctionType( JSTypeRegistry registry, String name, Node source, ArrowType arrowType, JSType typeOfThis, TemplateTypeMap templateTypeMap, boolean isConstructor, boolean nativeType) { super( registry, name, registry.getNativeObjectType(JSTypeNative.FUNCTION_INSTANCE_TYPE), nativeType, templateTypeMap); setPrettyPrint(true); Preconditions.checkArgument(source == null || Token.FUNCTION == source.getType()); Preconditions.checkNotNull(arrowType); this.source = source; if (isConstructor) { this.kind = Kind.CONSTRUCTOR; this.propAccess = PropAccess.ANY; this.typeOfThis = typeOfThis != null ? typeOfThis : new InstanceObjectType(registry, this, nativeType); } else { this.kind = Kind.ORDINARY; this.typeOfThis = typeOfThis != null ? typeOfThis : registry.getNativeObjectType(JSTypeNative.UNKNOWN_TYPE); } this.call = arrowType; }
@Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getType()) { // Function calls case Token.CALL: Node child = n.getFirstChild(); String name = null; // NOTE: The normalization pass insures that local names do not // collide with global names. if (child.isName()) { name = child.getString(); } else if (child.isFunction()) { name = anonFunctionMap.get(child); } else if (NodeUtil.isFunctionObjectCall(n)) { Preconditions.checkState(NodeUtil.isGet(child)); Node fnIdentifingNode = child.getFirstChild(); if (fnIdentifingNode.isName()) { name = fnIdentifingNode.getString(); } else if (fnIdentifingNode.isFunction()) { name = anonFunctionMap.get(fnIdentifingNode); } } if (name != null) { FunctionState fs = functionMap.get(name); // Only visit call-sites for functions that can be inlined. if (fs != null) { callback.visitCallSite(t, n, fs); } } break; } }
public void findNamedFunctions(NodeTraversal t, Node n, Node parent) { if (!NodeUtil.isStatement(n)) { // There aren't any interesting functions here. return; } switch (n.getType()) { // Functions expressions in the form of: // var fooFn = function(x) { return ... } case Token.VAR: Preconditions.checkState(n.hasOneChild()); Node nameNode = n.getFirstChild(); if (nameNode.isName() && nameNode.hasChildren() && nameNode.getFirstChild().isFunction()) { maybeAddFunction(new FunctionVar(n), t.getModule()); } break; // Named functions // function Foo(x) { return ... } case Token.FUNCTION: Preconditions.checkState(NodeUtil.isStatementBlock(parent) || parent.isLabel()); if (!NodeUtil.isFunctionExpression(n)) { Function fn = new NamedFunction(n); maybeAddFunction(fn, t.getModule()); } break; } }
@Override Node optimizeSubtree(Node subtree) { switch (subtree.getType()) { case CALL: return tryFoldCall(subtree); case NEW: return tryFoldCtorCall(subtree); case TYPEOF: return tryFoldTypeof(subtree); case NOT: case POS: case NEG: case BITNOT: tryReduceOperandsForOp(subtree); return tryFoldUnaryOperator(subtree); case VOID: return tryReduceVoid(subtree); default: tryReduceOperandsForOp(subtree); return tryFoldBinaryOperator(subtree); } }
/** Try to fold array-length. e.g [1, 2, 3].length ==> 3, [x, y].length ==> 2 */ private Node tryFoldGetProp(Node n, Node left, Node right) { Preconditions.checkArgument(n.isGetProp()); if (left.isObjectLit()) { return tryFoldObjectPropAccess(n, left, right); } if (right.isString() && right.getString().equals("length")) { int knownLength = -1; switch (left.getType()) { case ARRAYLIT: if (mayHaveSideEffects(left)) { // Nope, can't fold this, without handling the side-effects. return n; } knownLength = left.getChildCount(); break; case STRING: knownLength = left.getString().length(); break; default: // Not a foldable case, forget it. return n; } Preconditions.checkState(knownLength != -1); Node lengthNode = IR.number(knownLength); n.getParent().replaceChild(n, lengthNode); reportCodeChange(); return lengthNode; } return n; }