/* * 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() { } }