/* * Copyright 2010 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.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.StaticSourceFile; import com.google.javascript.rhino.jstype.FunctionType; import com.google.javascript.rhino.jstype.JSType; import com.google.javascript.rhino.jstype.ObjectType; import java.util.Collection; import java.util.Comparator; import java.util.TreeSet; import javax.annotation.Nullable; /** * Inserts run-time type assertions. * * <p>We add markers to user-defined interfaces and classes in order to check if * an object conforms to that type. * * <p>For each function, we insert a run-time type assertion for each parameter * and return value for which the compiler has a type. * * <p>The JavaScript code which implements the type assertions is in * js/runtime-type-check.js. * */ class RuntimeTypeCheck implements CompilerPass { private static final Comparator<JSType> ALPHA = new Comparator<JSType>() { @Override public int compare(JSType t1, JSType t2) { return getName(t1).compareTo(getName(t2)); } private String getName(JSType type) { if (type.isInstanceType()) { return ((ObjectType) type).getReferenceName(); } else if (type.isNullType() || type.isBooleanValueType() || type.isNumberValueType() || type.isStringValueType() || type.isVoidType()) { return type.toString(); } else { // Type unchecked at runtime, so we don't care about the sorting order. return ""; } } }; private final AbstractCompiler compiler; private final String logFunction; RuntimeTypeCheck(AbstractCompiler compiler, @Nullable String logFunction) { this.compiler = compiler; this.logFunction = logFunction; } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, root, new AddMarkers(compiler)); NodeTraversal.traverseEs6(compiler, root, new AddChecks()); addBoilerplateCode(); new Normalize(compiler, false).process(externs, root); } /** * Inserts marker properties for user-defined interfaces and classes. * * <p>For example, for a class C, we add * {@code C.prototype['instance_of__C']}, and for each interface I it * implements , we add {@code C.prototype['implements__I']}. * * <p>Since interfaces are not a run-time JS concept, we use these markers to * recognize an interface implementation at runtime. We also use markers for * user-defined classes, so that we can easily recognize them independently of * which module they are defined in and whether the module is loaded. */ private static class AddMarkers extends NodeTraversal.AbstractPostOrderCallback { private final AbstractCompiler compiler; private AddMarkers(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isFunction()) { visitFunction(n); } } private void visitFunction(Node n) { FunctionType funType = n.getJSType().toMaybeFunctionType(); if (funType != null && !funType.isConstructor()) { return; } Node nodeToInsertAfter = findNodeToInsertAfter(n); nodeToInsertAfter = addMarker(funType, nodeToInsertAfter, null); TreeSet<ObjectType> stuff = new TreeSet<>(ALPHA); Iterables.addAll(stuff, funType.getAllImplementedInterfaces()); for (ObjectType interfaceType : stuff) { nodeToInsertAfter = addMarker(funType, nodeToInsertAfter, interfaceType); } } private Node addMarker( FunctionType funType, Node nodeToInsertAfter, @Nullable ObjectType interfaceType) { if (funType.getSource() == null) { return nodeToInsertAfter; } String className = NodeUtil.getName(funType.getSource()); // This can happen with anonymous classes declared with the type // {@code Function}. if (className == null) { return nodeToInsertAfter; } Node classNode = NodeUtil.newQName( compiler, className); Node marker = IR.string( interfaceType == null ? "instance_of__" + className : "implements__" + interfaceType.getReferenceName()); Node assign = IR.exprResult(IR.assign( IR.getelem( IR.getprop( classNode, IR.string("prototype")), marker), IR.trueNode())); nodeToInsertAfter.getParent().addChildAfter(assign, nodeToInsertAfter); compiler.reportChangeToEnclosingScope(assign); nodeToInsertAfter = assign; return nodeToInsertAfter; } /** * Find the node to insert the markers after. Typically, this node * corresponds to the constructor declaration, but we want to skip any of * the white-listed function calls. * * @param n the constructor function node * @return the node to insert after */ private Node findNodeToInsertAfter(Node n) { Node nodeToInsertAfter = findEnclosingConstructorDeclaration(n); Node next = nodeToInsertAfter.getNext(); while (next != null && isClassDefiningCall(next)) { nodeToInsertAfter = next; next = nodeToInsertAfter.getNext(); } return nodeToInsertAfter; } private static Node findEnclosingConstructorDeclaration(Node n) { while (!n.getParent().isScript() && !n.getParent().isNormalBlock()) { n = n.getParent(); } return n; } private boolean isClassDefiningCall(Node next) { return NodeUtil.isExprCall(next) && compiler.getCodingConvention().getClassesDefinedByCall( next.getFirstChild()) != null; } } /** * Insert calls to the run-time type checking function {@code checkType}, which * takes an expression to check and a list of checkers (one of which must * match). It returns the expression back to facilitate checking of return * values. We have checkers for value types, class types (user-defined and * externed), and interface types. */ private class AddChecks extends NodeTraversal.AbstractPostOrderCallback { private AddChecks() { } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (NodeUtil.isInSyntheticScript(n)) { return; } if (n.isFunction()) { visitFunction(n); } else if (n.isReturn()) { visitReturn(t, n); } } /** * Insert checks for the parameters of the function. */ private void visitFunction(Node n) { FunctionType funType = JSType.toMaybeFunctionType(n.getJSType()); Node block = n.getLastChild(); Node paramName = NodeUtil.getFunctionParameters(n).getFirstChild(); Node insertionPoint = null; // To satisfy normalization constraints, the type checking must be // added after any inner function declarations. for (Node next = block.getFirstChild(); next != null && NodeUtil.isFunctionDeclaration(next); next = next.getNext()) { insertionPoint = next; } for (Node paramType : funType.getParameters()) { // Can this ever happen? if (paramName == null) { return; } Node checkNode = createCheckTypeCallNode( paramType.getJSType(), paramName.cloneTree()); if (checkNode == null) { // We don't know how to check this parameter type. paramName = paramName.getNext(); continue; } checkNode = IR.exprResult(checkNode); if (insertionPoint == null) { block.addChildToFront(checkNode); } else { block.addChildAfter(checkNode, insertionPoint); } compiler.reportChangeToEnclosingScope(block); paramName = paramName.getNext(); insertionPoint = checkNode; } } private void visitReturn(NodeTraversal t, Node n) { Node function = t.getEnclosingFunction(); FunctionType funType = function.getJSType().toMaybeFunctionType(); Node retValue = n.getFirstChild(); if (retValue == null) { return; } Node checkNode = createCheckTypeCallNode( funType.getReturnType(), retValue.cloneTree()); if (checkNode == null) { return; } n.replaceChild(retValue, checkNode); t.reportCodeChange(); } /** * Creates a function call to check that the given expression matches the * given type at runtime. * * <p>For example, if the type is {@code (string|Foo)}, the function call is * {@code checkType(expr, [valueChecker('string'), classChecker('Foo')])}. * * @return the function call node or {@code null} if the type is not checked */ private Node createCheckTypeCallNode(JSType type, Node expr) { Node arrayNode = IR.arraylit(); Collection<JSType> alternates; if (type.isUnionType()) { alternates = new TreeSet<>(ALPHA); alternates.addAll(type.toMaybeUnionType().getAlternates()); } else { alternates = ImmutableList.of(type); } for (JSType alternate : alternates) { Node checkerNode = createCheckerNode(alternate); if (checkerNode == null) { return null; } arrayNode.addChildToBack(checkerNode); } return IR.call(jsCode("checkType"), expr, arrayNode); } /** * Creates a node which evaluates to a checker for the given type (which * must not be a union). We have checkers for value types, classes and * interfaces. * * @return the checker node or {@code null} if the type is not checked */ private Node createCheckerNode(JSType type) { if (type.isNullType()) { return jsCode("nullChecker"); } else if (type.isBooleanValueType() || type.isNumberValueType() || type.isStringValueType() || type.isVoidType()) { return IR.call( jsCode("valueChecker"), IR.string(type.toString())); } else if (type.isInstanceType()) { ObjectType objType = (ObjectType) type; String refName = objType.getReferenceName(); if (refName.equals("Object")) { return jsCode("objectChecker"); } StaticSourceFile sourceFile = NodeUtil.getSourceFile(objType.getConstructor().getSource()); if (sourceFile == null || sourceFile.isExtern()) { return IR.call( jsCode("externClassChecker"), IR.string(refName)); } return IR.call( jsCode(objType.getConstructor().isInterface() ? "interfaceChecker" : "classChecker"), IR.string(refName)); } else if (type.isFunctionType()) { return IR.call(jsCode("valueChecker"), IR.string("function")); } else { // We don't check this type (e.g. unknown & all types). return null; } } } private void addBoilerplateCode() { Node newNode = compiler.ensureLibraryInjected("runtime_type_check", false); if (newNode != null && logFunction != null) { // Inject the custom log function. Node logOverride = IR.exprResult( IR.assign( NodeUtil.newQName( compiler, "$jscomp.typecheck.log"), NodeUtil.newQName( compiler, logFunction))); newNode.getParent().addChildAfter(logOverride, newNode); compiler.reportChangeToEnclosingScope(newNode); } } private Node jsCode(String prop) { return NodeUtil.newQName( compiler, "$jscomp.typecheck." + prop); } }