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