blob: 6011d45ff5a2e06ab90dc97826d59b880d110cfa [file] [log] [blame]
/*
* Copyright 2014 Google Inc.
*
* 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.gwt.dev.js;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.HasJsInfo.JsMemberType;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.js.ast.JsArrayLiteral;
import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsBinaryOperator;
import com.google.gwt.dev.js.ast.JsBlock;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsInvocation;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsNew;
import com.google.gwt.dev.js.ast.JsNode;
import com.google.gwt.dev.js.ast.JsNullLiteral;
import com.google.gwt.dev.js.ast.JsParameter;
import com.google.gwt.dev.js.ast.JsReturn;
import com.google.gwt.dev.js.ast.JsScope;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsThisRef;
import com.google.gwt.dev.util.StringInterner;
import com.google.gwt.thirdparty.guava.common.base.Preconditions;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
/**
* Utils for JS AST.
*/
public class JsUtils {
/**
* Given a JsInvocation, determine if it is invoking a JsFunction that is
* specified to be executed only once during the program's lifetime.
*/
public static JsFunction isExecuteOnce(JsInvocation invocation) {
JsFunction f = isFunction(invocation.getQualifier());
if (f != null && f.isClinit()) {
return f;
}
return null;
}
/**
* Given an expression, determine if it is a JsNameRef that refers to a
* statically-defined JsFunction.
*/
public static JsFunction isFunction(JsExpression e) {
if (!(e instanceof JsNameRef)) {
return null;
}
JsNameRef ref = (JsNameRef) e;
// Unravel foo.call(...).
if (!ref.getName().isObfuscatable() && CALL_STRING.equals(ref.getIdent())) {
if (ref.getQualifier() instanceof JsNameRef) {
ref = (JsNameRef) ref.getQualifier();
}
}
JsNode staticRef = ref.getName().getStaticRef();
if (staticRef instanceof JsFunction) {
return (JsFunction) staticRef;
}
return null;
}
public static JsExpression createAssignment(JsExpression lhs, JsExpression rhs) {
return createAssignment(lhs.getSourceInfo(), lhs, rhs);
}
public static JsExpression createAssignment(SourceInfo info, JsExpression lhs, JsExpression rhs) {
return new JsBinaryOperation(info, JsBinaryOperator.ASG, lhs, rhs);
}
public static JsFunction createBridge(JMethod method, JsName polyName, JsScope scope) {
SourceInfo sourceInfo = method.getSourceInfo();
JsFunction bridge = new JsFunction(sourceInfo, scope);
for (JParameter p : method.getParams()) {
JsName name = bridge.getScope().declareName(p.getName());
bridge.getParameters().add(new JsParameter(sourceInfo, name));
}
JsNameRef reference = polyName.makeQualifiedRef(sourceInfo, new JsThisRef(sourceInfo));
List<JsExpression> args = Lists.newArrayList();
for (JsParameter p : bridge.getParameters()) {
args.add(p.getName().makeRef(sourceInfo));
}
JsExpression invocation = createInvocationOrPropertyAccess(
InvocationStyle.NORMAL, sourceInfo, method, reference.getQualifier(), reference, args);
JsBlock block = new JsBlock(sourceInfo);
if (method.getType() == JPrimitiveType.VOID) {
block.getStatements().add(invocation.makeStmt());
} else {
block.getStatements().add(new JsReturn(sourceInfo, invocation));
}
bridge.setBody(block);
return bridge;
}
public static JsExpression createCommaExpression(JsExpression... expressions) {
return createCommaExpressionHelper(expressions, 0);
}
private static JsExpression createCommaExpressionHelper(JsExpression[] expressions, int index) {
int remainingExpressions = expressions.length - index;
assert remainingExpressions >= 2;
JsExpression lhs = expressions[index];
JsExpression rhs = expressions[index + 1];
if (remainingExpressions > 2) {
rhs = createCommaExpressionHelper(expressions, index + 1);
}
// Construct the binary expression
if (rhs == null) {
return lhs;
} else if (lhs == null) {
return rhs;
}
return new JsBinaryOperation(lhs.getSourceInfo(), JsBinaryOperator.COMMA, lhs, rhs);
}
public static JsFunction createEmptyFunctionLiteral(SourceInfo info, JsScope scope, JsName name) {
JsFunction func = new JsFunction(info, scope, name);
func.setBody(new JsBlock(info));
return func;
}
public static JsExpression createQualifiedNameRef(
SourceInfo info, JsExpression base, String... names) {
JsExpression result = base;
for (String name : names) {
JsNameRef nameRef = new JsNameRef(info, name);
nameRef.setQualifier(result);
result = nameRef;
}
return result;
}
/**
* Given a string qualifier such as 'foo.bar.Baz', returns a chain of JsNameRef's representing
* this qualifier.
*/
public static JsNameRef createQualifiedNameRef(String namespace, SourceInfo sourceInfo) {
assert !namespace.isEmpty();
JsNameRef ref = null;
for (String part : namespace.split("\\.")) {
JsNameRef newRef = new JsNameRef(sourceInfo, part);
if (ref != null) {
newRef.setQualifier(ref);
}
ref = newRef;
}
return ref;
}
public static JsNameRef createQualifiedNameRef(SourceInfo info, JsName... names) {
JsNameRef result = null;
for (JsName name : names) {
if (result == null) {
result = name.makeRef(info);
continue;
}
result = name.makeQualifiedRef(info, result);
}
return result;
}
private enum TargetType {
SETTER, GETTER, NEWINSTANCE, FUNCTION, METHOD
}
private enum CallStyle {
DIRECT, USING_CALL, USING_APPLY_FOR_VARARGS_ARRAY
}
private static class InvocationDescriptor {
private final TargetType targetType;
private final CallStyle callStyle;
private final List<JsExpression> nonVarargsArguments;
private final JsExpression varargsArgument;
private final JsExpression instance;
private final JsNameRef reference;
InvocationDescriptor(TargetType targetType, CallStyle callStyle,
JsExpression instance, JsNameRef reference,
List<JsExpression> nonVarargsArguments, JsExpression varargsArgument) {
this.targetType = targetType;
this.callStyle = callStyle;
this.nonVarargsArguments = nonVarargsArguments;
this.varargsArgument = varargsArgument;
this.instance = instance;
this.reference = reference;
}
}
/**
* Decides the type of invokation to perform, tranforming vararg calls into plain calls if
* possible.
*/
private static InvocationDescriptor createInvocationDescriptor(InvocationStyle invocationStyle,
JMethod method, JsExpression instance, JsNameRef reference, List<JsExpression> args) {
CallStyle callStyle = invocationStyle == InvocationStyle.SUPER
// JsFunctions that are accessed through an instance field need to be called using CALL to
// avoid accidentally binding "this" to the field's qualifier. See bug #9328.
|| invocationStyle == InvocationStyle.FUNCTION
&& instance instanceof JsNameRef
&& ((JsNameRef) instance).getQualifier() != null
? CallStyle.USING_CALL : CallStyle.DIRECT;
TargetType targetType;
switch (invocationStyle) {
case NEWINSTANCE:
assert method.isConstructor();
targetType = TargetType.NEWINSTANCE;
break;
case FUNCTION:
assert method.isOrOverridesJsFunctionMethod();
targetType = TargetType.FUNCTION;
break;
default:
if (method.getJsMemberType().isPropertyAccessor()) {
targetType = method.getJsMemberType() == JsMemberType.GETTER
? TargetType.GETTER : TargetType.SETTER;
} else {
targetType = TargetType.METHOD;
}
break;
}
JsExpression lastArgument = Iterables.getLast(args, null);
boolean needsVarargsApply =
method.isJsMethodVarargs() && !(lastArgument instanceof JsArrayLiteral);
List<JsExpression> nonVarargArguments = args;
JsExpression varargArgument = null;
if (method.isJsMethodVarargs()) {
nonVarargArguments = nonVarargArguments.subList(0, args.size() - 1);
if (!needsVarargsApply) {
nonVarargArguments.addAll(((JsArrayLiteral) lastArgument).getExpressions());
} else {
varargArgument = lastArgument;
callStyle = CallStyle.USING_APPLY_FOR_VARARGS_ARRAY;
}
}
instance = instance != null ? instance : JsNullLiteral.INSTANCE;
return new InvocationDescriptor(targetType, callStyle, instance, reference,
nonVarargArguments, varargArgument);
}
private static JsExpression prepareArgumentsForApply(SourceInfo sourceInfo,
Iterable<JsExpression> nonVarargsArguments, JsExpression varargsArgument) {
if (Iterables.isEmpty(nonVarargsArguments)) {
return varargsArgument;
}
JsArrayLiteral argumentsArray = new JsArrayLiteral(sourceInfo, nonVarargsArguments);
JsNameRef argumentsConcat = new JsNameRef(sourceInfo,"concat");
argumentsConcat.setQualifier(argumentsArray);
return new JsInvocation(sourceInfo, argumentsConcat, varargsArgument);
}
public static JsExpression createApplyInvocation(
SourceInfo sourceInfo, InvocationDescriptor invocationDescriptor) {
assert invocationDescriptor.callStyle == CallStyle.USING_APPLY_FOR_VARARGS_ARRAY;
switch (invocationDescriptor.targetType) {
case FUNCTION:
// fn.apply(null, [p1, ..., pn].concat(varargsArray));
return new JsInvocation(sourceInfo,
createQualifiedNameRef(sourceInfo, invocationDescriptor.instance, "apply"),
JsNullLiteral.INSTANCE,
prepareArgumentsForApply(sourceInfo,
invocationDescriptor.nonVarargsArguments,
invocationDescriptor.varargsArgument));
case METHOD:
// Static method:
// q.name.apply(null, [p1, ..., pn].concat(varargsArray));
// Instance method:
// instance.name.apply(instance, [p1, ..., pn].concat(varargsArray));
// Super call:
// q.name.apply(instance, [p1, ..., pn].concat(varargsArray));
JsExpression instance = invocationDescriptor.instance;
if (instance == invocationDescriptor.reference.getQualifier()) {
// If instance == qualifier, instance needs to be cloned as it can not appear in two
// places in the JS AST. This needs to be done only in the case of VARRAGS_ARRAY.
// Instance here has been normalized to be just a "leaf" JsNameRef by
// {@link ImplementJsVarargs} so that the following translation can be avoided here.
// (_t = instance).name.apply(_t, [p1, ..., pn].concat(varargsArray));
assert (instance instanceof JsNameRef && ((JsNameRef) instance).isLeaf());
instance = Preconditions.checkNotNull(JsSafeCloner.clone(instance));
}
return new JsInvocation(sourceInfo,
createQualifiedNameRef(sourceInfo, invocationDescriptor.reference, "apply"),
instance,
prepareArgumentsForApply(sourceInfo,
invocationDescriptor.nonVarargsArguments,
invocationDescriptor.varargsArgument));
case NEWINSTANCE:
// new (q.name.bind.apply(q, [null, p1, ... pn])())()
return new JsNew(sourceInfo, new JsInvocation(sourceInfo,
createQualifiedNameRef(sourceInfo, invocationDescriptor.reference, "bind", "apply"),
invocationDescriptor.reference,
prepareArgumentsForApply(sourceInfo,
Iterables.concat(
Collections.singleton(JsNullLiteral.INSTANCE),
invocationDescriptor.nonVarargsArguments),
invocationDescriptor.varargsArgument)));
default:
throw new AssertionError("Target type " + invocationDescriptor.targetType
+ " invalid for varargs apply invocation");
}
}
public static JsExpression createDirectInvocationOrPropertyAccess(
SourceInfo sourceInfo, InvocationDescriptor invocationDescriptor) {
assert invocationDescriptor.callStyle == CallStyle.DIRECT;
switch (invocationDescriptor.targetType) {
case SETTER:
assert invocationDescriptor.nonVarargsArguments.size() == 1;
return createAssignment(invocationDescriptor.reference,
invocationDescriptor.nonVarargsArguments.get(0));
case GETTER:
assert invocationDescriptor.nonVarargsArguments.size() == 0;
return invocationDescriptor.reference;
case FUNCTION:
return new JsInvocation(sourceInfo, invocationDescriptor.instance,
invocationDescriptor.nonVarargsArguments);
case METHOD:
return new JsInvocation(sourceInfo, invocationDescriptor.reference,
invocationDescriptor.nonVarargsArguments);
case NEWINSTANCE:
return new JsNew(
sourceInfo, invocationDescriptor.reference, invocationDescriptor.nonVarargsArguments);
default:
throw new AssertionError("Target type " + invocationDescriptor.targetType
+ " invalid for direct invocation");
}
}
public static JsExpression createCallInvocationOrSuperPropertyAccess(
SourceInfo sourceInfo, InvocationDescriptor invocationDescriptor) {
assert invocationDescriptor.callStyle == CallStyle.USING_CALL;
switch (invocationDescriptor.targetType) {
case SETTER:
assert invocationDescriptor.nonVarargsArguments.size() == 1;
// TODO(rluble): implement super setters.
throw new UnsupportedOperationException("Super.setter is unsupported");
case GETTER:
assert invocationDescriptor.nonVarargsArguments.size() == 0;
// TODO(rluble): implement super getters.
throw new UnsupportedOperationException("Super.getter is unsupported");
case FUNCTION:
// instance.call(null, p1, ..., pn)
return createCallInvocation(sourceInfo, invocationDescriptor.instance,
JsNullLiteral.INSTANCE, invocationDescriptor.nonVarargsArguments);
case METHOD:
// q.methodname.call(instance, p1, ..., pn)
return createCallInvocation(sourceInfo, invocationDescriptor.reference,
invocationDescriptor.instance, invocationDescriptor.nonVarargsArguments);
default:
throw new AssertionError("Target type " + invocationDescriptor.targetType
+ " invalid for super invocation");
}
}
/**
* Synthesize an invocation using .call().
*/
private static JsInvocation createCallInvocation(SourceInfo sourceInfo, JsExpression target,
JsExpression instance, Iterable<JsExpression> arguments) {
return new JsInvocation(sourceInfo, createQualifiedNameRef(sourceInfo, target, "call"),
Iterables.concat(Collections.singleton(instance),arguments));
}
/**
* Invocation styles.
*/
public enum InvocationStyle {
NORMAL, FUNCTION, SUPER, NEWINSTANCE
}
public static JsExpression createInvocationOrPropertyAccess(InvocationStyle invocationStyle,
SourceInfo sourceInfo, JMethod method, JsExpression instance, JsNameRef reference,
List<JsExpression> args) {
InvocationDescriptor invocationDescriptor =
createInvocationDescriptor(invocationStyle, method, instance, reference, args);
switch (invocationDescriptor.callStyle) {
case DIRECT:
return createDirectInvocationOrPropertyAccess(sourceInfo, invocationDescriptor);
case USING_CALL:
return createCallInvocationOrSuperPropertyAccess(sourceInfo, invocationDescriptor);
case USING_APPLY_FOR_VARARGS_ARRAY:
return createApplyInvocation(sourceInfo, invocationDescriptor);
}
throw new AssertionError();
}
/**
* Attempts to extract a single expression from a given statement and returns
* it. If no such expression exists, returns <code>null</code>.
*/
public static JsExpression extractExpression(JsStatement stmt) {
if (stmt == null) {
return null;
}
if (stmt instanceof JsExprStmt) {
return ((JsExprStmt) stmt).getExpression();
}
if (stmt instanceof JsBlock && ((JsBlock) stmt).getStatements().size() == 1) {
return extractExpression(((JsBlock) stmt).getStatements().get(0));
}
return null;
}
public static JsName getJsNameForMethod(JavaToJavaScriptMap jjsmap, JProgram jprogram,
String indexedMethodName) {
return jjsmap.nameForMethod(jprogram.getIndexedMethod(indexedMethodName));
}
public static JsName getJsNameForField(JavaToJavaScriptMap jjsmap, JProgram jprogram,
String indexedMethodName) {
return jjsmap.nameForField(jprogram.getIndexedField(indexedMethodName));
}
public static boolean isEmpty(JsStatement stmt) {
if (stmt == null) {
return true;
}
return (stmt instanceof JsBlock && ((JsBlock) stmt).getStatements().isEmpty());
}
/**
* If the statement is a JsExprStmt that declares a function with no other
* side effects, returns that function; otherwise <code>null</code>.
*/
public static JsFunction isFunctionDeclaration(JsStatement stmt) {
if (stmt instanceof JsExprStmt) {
JsExprStmt exprStmt = (JsExprStmt) stmt;
JsExpression expr = exprStmt.getExpression();
if (expr instanceof JsFunction) {
JsFunction func = (JsFunction) expr;
if (func.getName() != null) {
return func;
}
}
}
return null;
}
/**
* A JavaScript identifier contains only letters, numbers, _, $ and does not begin with a number.
* There are actually other valid identifiers, such as ones that contain escaped Unicode
* characters but we disallow those for the time being.
*/
public static boolean isValidJsIdentifier(String name) {
return JAVASCRIPT_VALID_IDENTIFIER_PATTERN.matcher(name).matches();
}
public static boolean isValidJsQualifiedName(String name) {
return JAVASCRIPT_VALID_QUALIFIED_NAME_PATTERN.matcher(name).matches();
}
private static final String VALID_JS_NAME_REGEX = "[a-zA-Z_$][\\w_$]*";
private static final Pattern JAVASCRIPT_VALID_QUALIFIED_NAME_PATTERN =
Pattern.compile(VALID_JS_NAME_REGEX + "(\\." + VALID_JS_NAME_REGEX + ")*");
private static final Pattern JAVASCRIPT_VALID_IDENTIFIER_PATTERN =
Pattern.compile(VALID_JS_NAME_REGEX);
private static final String CALL_STRING = StringInterner.get().intern("call");
private JsUtils() {
}
}