com.google.javascript.jscomp.AngularPass Maven / Gradle / Ivy
/*
* 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 static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.List;
/**
* Compiler pass for AngularJS-specific needs. Generates {@code $inject} \
* properties for functions (class constructors, wrappers, etc) annotated with
* @ngInject. Without this pass, AngularJS will not work properly if variable
* renaming is enabled, because the function arguments will be renamed.
* @see http://docs.angularjs.org/tutorial/step_05#a-note-on-minification
*
* For example, the following code:
*
*
* /** @ngInject * /
* function Controller(dependency1, dependency2) {
* // do something
* }
*
*
*
* will be transformed into:
*
*
* function Controller(dependency1, dependency2) {
* // do something
* }
* Controller.$inject = ['dependency1', 'dependency2'];
*
*
*
* This pass also supports assignments of function expressions to variables
* like:
*
*
* /** @ngInject * /
* var filter = function(a, b) {};
*
* var ns = {};
* /** @ngInject * /
* ns.method = function(a,b,c) {};
*
* /** @ngInject * /
* var shorthand = ns.method2 = function(a,b,c,) {}
*
*
*/
class AngularPass extends AbstractPostOrderCallback
implements HotSwapCompilerPass {
final AbstractCompiler compiler;
/** Nodes annotated with @ngInject */
private final List injectables = new ArrayList<>();
public AngularPass(AbstractCompiler compiler) {
this.compiler = compiler;
}
public static final String INJECT_PROPERTY_NAME = "$inject";
static final DiagnosticType INJECT_IN_NON_GLOBAL_OR_BLOCK_ERROR =
DiagnosticType.error("JSC_INJECT_IN_NON_GLOBAL_OR_BLOCK_ERROR",
"@ngInject only applies to functions defined in blocks or " +
"global scope.");
static final DiagnosticType INJECT_NON_FUNCTION_ERROR =
DiagnosticType.error("JSC_INJECT_NON_FUNCTION_ERROR",
"@ngInject can only be used when defining a function or " +
"assigning a function expression.");
static final DiagnosticType INJECTED_FUNCTION_HAS_DESTRUCTURED_PARAM =
DiagnosticType.error("JSC_INJECTED_FUNCTION_HAS_DESTRUCTURED_PARAM",
"@ngInject cannot be used on functions containing "
+ "destructured parameter.");
static final DiagnosticType INJECTED_FUNCTION_HAS_DEFAULT_VALUE =
DiagnosticType.error("JSC_INJECTED_FUNCTION_HAS_DEFAULT_VALUE",
"@ngInject cannot be used on functions containing default value.");
static final DiagnosticType INJECTED_FUNCTION_ON_NON_QNAME =
DiagnosticType.error("JSC_INJECTED_FUNCTION_ON_NON_QNAME",
"@ngInject can only be used on qualified names.");
@Override
public void process(Node externs, Node root) {
hotSwapScript(root, null);
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
// Traverses AST looking for nodes annotated with @ngInject.
NodeTraversal.traverse(compiler, scriptRoot, this);
// iterates through annotated nodes adding $inject property to elements.
for (NodeContext entry : injectables) {
String name = entry.getName();
Node fn = entry.getFunctionNode();
List dependencies = createDependenciesList(fn);
// skips entry if it does not have any dependencies.
if (dependencies.isEmpty()) {
continue;
}
Node dependenciesArray = IR.arraylit(dependencies.toArray(new Node[0]));
// creates `something.$inject = ['param1', 'param2']` node.
Node statement = IR.exprResult(
IR.assign(
IR.getelem(
NodeUtil.newQName(compiler, name),
IR.string(INJECT_PROPERTY_NAME)),
dependenciesArray
)
);
statement.useSourceInfoFromForTree(entry.getNode());
statement.setOriginalName(name);
// Set the visibility of the newly created property.
JSDocInfoBuilder newPropertyDoc = new JSDocInfoBuilder(false);
newPropertyDoc.recordVisibility(Visibility.PUBLIC);
statement.getFirstChild().setJSDocInfo(newPropertyDoc.build());
// adds `something.$inject = [...]` node after the annotated node or the following
// goog.inherits call.
Node insertionPoint = entry.getTarget();
Node next = insertionPoint.getNext();
while (next != null
&& NodeUtil.isExprCall(next)
&& compiler.getCodingConvention().getClassesDefinedByCall(
next.getFirstChild()) != null) {
insertionPoint = next;
next = insertionPoint.getNext();
}
insertionPoint.getParent().addChildAfter(statement, insertionPoint);
compiler.reportChangeToEnclosingScope(statement);
}
}
/**
* Given a FUNCTION node returns array of STRING nodes representing function
* parameters.
* @param n the FUNCTION node.
* @return STRING nodes.
*/
private List createDependenciesList(Node n) {
checkArgument(n.isFunction());
Node params = NodeUtil.getFunctionParameters(n);
if (params != null) {
return createStringsFromParamList(params);
}
return new ArrayList<>();
}
/**
* Given a PARAM_LIST node creates an array of corresponding STRING nodes.
* @param params PARAM_LIST node.
* @return array of STRING nodes.
*/
private List createStringsFromParamList(Node params) {
Node param = params.getFirstChild();
ArrayList names = new ArrayList<>();
while (param != null) {
if (param.isName()) {
names.add(IR.string(param.getString()).srcref(param));
} else if (param.isDestructuringPattern()) {
compiler.report(JSError.make(param,
INJECTED_FUNCTION_HAS_DESTRUCTURED_PARAM));
return new ArrayList<>();
} else if (param.isDefaultValue()) {
compiler.report(JSError.make(param,
INJECTED_FUNCTION_HAS_DEFAULT_VALUE));
return new ArrayList<>();
}
param = param.getNext();
}
return names;
}
@Override
public void visit(NodeTraversal unused, Node n, Node parent) {
JSDocInfo docInfo = n.getJSDocInfo();
if (docInfo != null && docInfo.isNgInject()) {
addNode(n);
}
}
/**
* Add node to the list of injectables.
*
* @param n node to add.
*/
private void addNode(Node n) {
Node target = null;
Node fn = null;
String name = null;
switch (n.getToken()) {
// handles assignment cases like:
// a = function() {}
// a = b = c = function() {}
case ASSIGN:
if (!n.getFirstChild().isQualifiedName()) {
compiler.report(JSError.make(n, INJECTED_FUNCTION_ON_NON_QNAME));
return;
}
name = n.getFirstChild().getQualifiedName();
// last node of chained assignment.
fn = n;
while (fn.isAssign()) {
fn = fn.getLastChild();
}
target = n.getParent();
break;
// handles function case:
// function fnName() {}
case FUNCTION:
name = NodeUtil.getName(n);
fn = n;
target = n;
if (n.getParent().isAssign()
&& n.getParent().getJSDocInfo().isNgInject()) {
// This is a function assigned into a symbol, e.g. a regular function
// declaration in a goog.module or goog.scope.
// Skip in this traversal, it is handled when visiting the assign.
return;
}
break;
// handles var declaration cases like:
// var a = function() {}
// var a = b = function() {}
case VAR:
case LET:
case CONST:
name = n.getFirstChild().getString();
// looks for a function node.
fn = getDeclarationRValue(n);
target = n;
break;
// handles class method case:
// class clName(){
// constructor(){}
// someMethod(){} <===
// }
case MEMBER_FUNCTION_DEF:
Node parent = n.getParent();
if (parent.isClassMembers()){
Node classNode = parent.getParent();
String midPart = n.isStaticMember() ? "." : ".prototype.";
name = NodeUtil.getName(classNode) + midPart + n.getString();
if (NodeUtil.isEs6ConstructorMemberFunctionDef(n)) {
name = NodeUtil.getName(classNode);
}
fn = n.getFirstChild();
if (classNode.getParent().isAssign() || classNode.getParent().isName()) {
target = classNode.getGrandparent();
} else {
target = classNode;
}
}
break;
default:
break;
}
if (fn == null || !fn.isFunction()) {
compiler.report(JSError.make(n, INJECT_NON_FUNCTION_ERROR));
return;
}
// report an error if the function declaration did not take place in a block or global scope
if (!target.getParent().isScript()
&& !target.getParent().isBlock()
&& !target.getParent().isModuleBody()) {
compiler.report(JSError.make(n, INJECT_IN_NON_GLOBAL_OR_BLOCK_ERROR));
return;
}
// checks that name is present, which must always be the case unless the
// compiler allowed a syntax error or a dangling anonymous function
// expression.
checkNotNull(name);
// registers the node.
injectables.add(new NodeContext(name, n, fn, target));
}
/**
* Given a VAR node (variable declaration) returns the node of initial value.
*
*
* var x; // null
* var y = "value"; // STRING "value" node
* var z = x = y = function() {}; // FUNCTION node
*
* @param n VAR node.
* @return the assigned initial value, or the rightmost rvalue of an assignment
* chain, or null.
*/
private static Node getDeclarationRValue(Node n) {
checkNotNull(n);
checkArgument(NodeUtil.isNameDeclaration(n));
n = n.getFirstFirstChild();
if (n == null) {
return null;
}
while (n.isAssign()) {
n = n.getLastChild();
}
return n;
}
static class NodeContext {
/** Name of the function/object. */
private final String name;
/** Node jsDoc is attached to. */
private final Node node;
/** Function node */
private final Node functionNode;
/** Node after which to inject the new code */
private final Node target;
public NodeContext(String name, Node node, Node functionNode, Node target) {
this.name = name;
this.node = node;
this.functionNode = functionNode;
this.target = target;
}
/**
* @return the name.
*/
public String getName() {
return name;
}
/**
* @return the node.
*/
public Node getNode() {
return node;
}
/**
* @return the context.
*/
public Node getFunctionNode() {
return functionNode;
}
/**
* @return the context.
*/
public Node getTarget() {
return target;
}
}
}