/* * Copyright 2009 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.javascript.jscomp.parsing.parser.util.format.SimpleFormat; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.Node; import java.util.regex.Pattern; /** * Generates goog.exportSymbol for test functions, so they can be recognized * by the test runner, even if the code is compiled. * */ public class ExportTestFunctions implements CompilerPass { private static final Pattern TEST_FUNCTIONS_NAME_PATTERN = Pattern.compile( "^(?:((\\w+\\.)+prototype\\.||window\\.)*" + "(setUpPage|setUp|shouldRunTests|tearDown|tearDownPage|test[\\w\\$]+))$"); private AbstractCompiler compiler; private final String exportSymbolFunction; private final String exportPropertyFunction; /** * Creates a new export test functions compiler pass. * @param compiler * @param exportSymbolFunction The function name used to export symbols in JS. * @param exportPropertyFunction The function name used to export properties * in JS. */ ExportTestFunctions(AbstractCompiler compiler, String exportSymbolFunction, String exportPropertyFunction) { Preconditions.checkNotNull(compiler); this.compiler = compiler; this.exportSymbolFunction = exportSymbolFunction; this.exportPropertyFunction = exportPropertyFunction; } private class ExportTestFunctionsNodes extends NodeTraversal.AbstractShallowCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (parent == null) { return; } if (parent.isScript()) { if (NodeUtil.isFunctionDeclaration(n)) { // Check for a test function statement. String functionName = NodeUtil.getName(n); if (isTestFunction(functionName)) { exportTestFunctionAsSymbol(functionName, n, parent); } } else if (isNameDeclaredFunction(n)) { // Check for a test function expression. Node functionNode = n.getFirstFirstChild(); String functionName = NodeUtil.getName(functionNode); if (isTestFunction(functionName)) { exportTestFunctionAsSymbol(functionName, n, parent); } } else if (n.isClass()) { exportClass(parent, n); } } else if (NodeUtil.isExprAssign(parent) && !n.getLastChild().isAssign()) { // Check for a test method assignment. Node grandparent = parent.getParent(); if (grandparent != null && grandparent.isScript()) { String functionName = n.getFirstChild().getQualifiedName(); if (isTestFunction(functionName)) { exportTestFunctionAsProperty(functionName, parent, n, grandparent); } } } else if (n.isObjectLit() && isCallTargetQName(n.getParent(), "goog.testing.testSuite")) { for (Node c : n.children()) { if (c.isStringKey() && !c.isQuotedString()) { c.setQuotedString(); compiler.reportChangeToEnclosingScope(c); } else if (c.isMemberFunctionDef()) { rewriteMemberDefInObjLit(c, n); } } } } private void exportClass(Node scriptNode, Node classNode) { Node classMembers = classNode.getLastChild(); for (Node maybeMemberFunctionDef : classMembers.children()) { if (maybeMemberFunctionDef.isMemberFunctionDef()) { String methodName = maybeMemberFunctionDef.getString(); if (TEST_FUNCTIONS_NAME_PATTERN.matcher(methodName).matches()) { String functionRef = NodeUtil.getName(classNode) + ".prototype." + methodName; String classRef = NodeUtil.getName(classNode) + ".prototype"; Node exportCallTarget = NodeUtil.newQName( compiler, exportPropertyFunction, maybeMemberFunctionDef, methodName); Node call = IR.call(exportCallTarget); if (exportCallTarget.isName()) { call.putBooleanProp(Node.FREE_CALL, true); } call.addChildToBack( NodeUtil.newQName(compiler, classRef, maybeMemberFunctionDef, classRef)); call.addChildToBack(IR.string(methodName)); call.addChildToBack( NodeUtil.newQName(compiler, functionRef, maybeMemberFunctionDef, functionRef)); Node expression = IR.exprResult(call); scriptNode.addChildAfter(expression, classNode); compiler.reportChangeToEnclosingScope(expression); } } } } private void rewriteMemberDefInObjLit(Node memberDef, Node objLit) { String name = memberDef.getString(); Node stringKey = IR.stringKey(name, memberDef.getFirstChild().detach()); objLit.replaceChild(memberDef, stringKey); stringKey.setQuotedString(); compiler.reportChangeToEnclosingScope(objLit); } // TODO(johnlenz): move test suite declaration into the // coding convention class. private boolean isCallTargetQName(Node n, String qname) { return (n.isCall() && n.getFirstChild().matchesQualifiedName(qname)); } /** * Whether node corresponds to a function expression declared with var, let * or const which is of the form: * <pre> * var/let/const functionName = function() { * // Implementation * }; * </pre> * This has the AST structure VAR/LET/CONST -> NAME -> FUNCTION * @param node */ private boolean isNameDeclaredFunction(Node node) { if (!NodeUtil.isNameDeclaration(node)) { return false; } Node grandchild = node.getFirstFirstChild(); return grandchild != null && grandchild.isFunction(); } } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, root, new ExportTestFunctionsNodes()); } // Adds exportSymbol(testFunctionName, testFunction); private void exportTestFunctionAsSymbol(String testFunctionName, Node node, Node scriptNode) { Node exportCallTarget = NodeUtil.newQName(compiler, exportSymbolFunction, node, testFunctionName); Node call = IR.call(exportCallTarget); if (exportCallTarget.isName()) { call.putBooleanProp(Node.FREE_CALL, true); } call.addChildToBack(IR.string(testFunctionName)); call.addChildToBack(NodeUtil.newQName(compiler, testFunctionName, node, testFunctionName)); Node expression = IR.exprResult(call); scriptNode.addChildAfter(expression, node); compiler.reportChangeToEnclosingScope(expression); } // Adds exportProperty() of the test function name on the prototype object private void exportTestFunctionAsProperty(String fullyQualifiedFunctionName, Node parent, Node node, Node scriptNode) { String testFunctionName = NodeUtil.getPrototypePropertyName(node.getFirstChild()); if (node.getFirstChild().getQualifiedName().startsWith("window.")) { testFunctionName = node.getFirstChild().getQualifiedName().substring("window.".length()); } String objectName = fullyQualifiedFunctionName.substring(0, fullyQualifiedFunctionName.lastIndexOf('.')); String exportCallStr = SimpleFormat.format("%s(%s, '%s', %s);", exportPropertyFunction, objectName, testFunctionName, fullyQualifiedFunctionName); Node exportCall = this.compiler.parseSyntheticCode(exportCallStr) .removeChildren(); exportCall.useSourceInfoFromForTree(scriptNode); scriptNode.addChildrenAfter(exportCall, parent); compiler.reportChangeToEnclosingScope(exportCall); } /** * Whether a function is recognized as a test function. We follow the JsUnit * convention for naming (functions should start with "test"), and we also * check if it has no parameters declared. * * @param functionName The name of the function * @return {@code true} if the function is recognized as a test function. */ public static boolean isTestFunction(String functionName) { return functionName != null && TEST_FUNCTIONS_NAME_PATTERN.matcher(functionName).matches(); } }